feat(frontend): gate bridge.cl8y.com with CL8Y Legal web clickwrap #134

Open
opened 2026-08-11 03:53:02 +00:00 by PlasticDigits · 11 comments
PlasticDigits commented 2026-08-11 03:53:02 +00:00 (Migrated from gitlab.com)

Summary

Integrate @plasticdigits/cl8y-clickwrap into packages/frontend so wallet-connected users on https://bridge.cl8y.com must accept the latest CL8Y Terms & Conditions (via the hosted portal at https://terms.cl8y.com) before they can use transfer / deposit / withdraw UI. Bundle dependency install, multi-network gating (EVM / Terra Classic / Solana), UX wiring, ops prerequisites, docs/invariants, and tests into this single issue.

Upstream SDK / portal: cl8y-ecosystem-legal — package packages/cl8y-clickwrap, API https://api.terms.cl8y.com, portal https://terms.cl8y.com.


Current codebase

The bridge frontend (packages/frontend) is a Vite + React 18 app with:

Area Behavior today
Entry / routing src/main.tsx — BrowserRouter + Layout outlet; routes /, /transfer/:xchainHashId, /history, /verify, /settings
Wallets EVM: wagmi (useAccount) via ConnectWallet / EvmWalletModal. Terra Classic: useWallet / stores/wallet. Solana: useSolanaWallet / stores/solanaWallet
Transfer gate today TransferForm already requires the source-chain wallet (isWalletConnected ≈ Terra / Solana / EVM by direction) before deposit CTAs — but there is no legal terms / signature-status check
Legal integration None. No dependency on @plasticdigits/cl8y-clickwrap, no calls to api.terms.cl8y.com, no redirect to terms.cl8y.com
Production host https://bridge.cl8y.com (see e.g. closed issue #119 walkthrough notes)
Hardening culture Frontend invariants in docs/FRONTEND_BRIDGE_INVARIANTS.md; Playwright under packages/frontend/e2e/; Vitest unit/integration under src/**

CL8Y Legal model (upstream):

  • Acceptance is per property (hostname). Signing for cl8y.com does not satisfy bridge.cl8y.com.
  • One signature row per (property, terms version, network, account).
  • Integrators check GET /api/v1/signatures/status (SDK: client.getSignatureStatus), then redirect unsigned users to the portal sign URL for that network.
  • Portal shows full terms + scroll + consent checkbox before wallet connect (upstream portal invariant; do not reimplement on the bridge).

SDK surface (intended):

import { createClient } from "@plasticdigits/cl8y-clickwrap";
import { TermsGate } from "@plasticdigits/cl8y-clickwrap/react";

TermsGate polls signature status, shows accept UI when signed_latest is false, redirects to the portal with redirect_uri + app_name, and re-checks on window focus after return.

Package availability note: As of this issue, @plasticdigits/cl8y-clickwrap@0.1.0 may not yet be on the public npm registry. Prefer published npm once available; until then, coordinate with Legal to publish, or temporarily install from the Legal repo tarball / git path (document the chosen path in the MR).


Why this is needed

  1. Legal / product requirement: Bridge users move real value across EVM, Terra Classic, and Solana. They must have an auditable, wallet-signed acceptance of the latest global T&Cs scoped to the bridge property before using the dapp.
  2. Consistency with CL8Y ecosystem: Other CL8Y sites are expected to use the same Legal API + clickwrap SDK rather than one-off checkboxes that are not cryptographically bound.
  3. Version bumps: When Legal publishes a new terms version, signed_latest becomes false and the bridge must re-gate until the user re-signs — without a custom versioning system in this monorepo.
  4. Do not DIY the portal: Full terms disclosure, Content-SHA256 message binding, and redirect allowlisting already live in Legal. The bridge should integrate, not fork, that UX.

Constraints & guardrails

  1. Property hostname: Register and use property bridge.cl8y.com (exact production host). Do not reuse cl8y.com acceptance. Confirm with Legal ops before merge.

  2. CORS: Legal API CORS_ORIGINS must include https://bridge.cl8y.com (and local Vite origin for dev, e.g. http://localhost:3000). Browser status checks will fail closed without this.

  3. Portal redirect_uri allowlist: Legal web VITE_REDIRECT_URI_ALLOWLIST must include https://bridge.cl8y.com so post-sign navigation returns to the bridge. Localhost only when Legal enables VITE_ALLOW_LOCALHOST_REDIRECT.

  4. Prefer SDK, do not reimplement: Use @plasticdigits/cl8y-clickwrap (createClient, TermsGate and/or useSignatureStatus + buildSignUrl). Do not copy portal sign-page DOM, do not invent a parallel local “I agree” checkbox that claims to satisfy Legal, and do not submit wallet signatures from the bridge unless Legal explicitly documents that path for integrators.

  5. Network mapping must match source wallet (same idea as TransferForm’s isWalletConnected):

    Source chain type SDK Network Account source
    EVM EVM wagmi address
    Terra Classic / cosmos TerraClassic Terra wallet store address
    Solana Solana Solana wallet store address
  6. No account → no hard block of connect UX: When no relevant wallet is connected, users must still be able to open wallet modals / browse read-only surfaces. Gate mutative bridge actions (deposit CTA, and any withdraw/execute paths that require the connected account) — not the entire app shell — unless product explicitly decides otherwise. Recommended default: wrap transfer action surface (and equivalent status-page submit/execute panels) so unsigned users see the TermsGate / accept CTA instead of deposit controls.

  7. UI consent is not auth: UI gate + portal redirect prove UX intent; server-side Legal still requires a valid wallet signature. Do not treat a local checkbox alone as acceptance.

  8. Safe rendering: Never dangerouslySetInnerHTML for terms API content or query params. Prefer portal for full text; if linking to GET .../terms/latest/content, open as plain text / external link.

  9. Redirect safety: Pass redirectUri={window.location.href} (or a same-origin path) only. Optionally preflight with SDK isAllowedRedirectUri for fail-fast UX; portal remains the enforcer.

  10. Fail closed on status errors for mutative actions: If signature status cannot be verified (network/CORS/5xx), do not silently enable Bridge / deposit / execute. Show a clear error + retry.

  11. Do not break existing frontend invariants in docs/FRONTEND_BRIDGE_INVARIANTS.md (recipient validation INV-RCP1, CTA validity INV-UX1, rate-limit UX, MegaETH chip, token logos, etc.).

  12. Under-construction mode: When VITE_UNDER_CONSTRUCTION === 'true', clickwrap is N/A (only UnderConstructionPage).

  13. Secrets: No new secret VITE_* keys. Public API base URLs only if overridden; default SDK bases (api.terms.cl8y.com / terms.cl8y.com) are fine for production.

  14. Solana portal readiness: Confirm Legal portal /sign/solana is live for production before requiring Solana source users to complete the redirect loop. If Solana portal is not ready, document a temporary product decision in the MR (block Solana deposits with explicit message vs delay Solana gating) — do not silently skip legal for Solana while gating EVM/Terra.

  15. AGPL / npm license: Bridge frontend is part of an AGPL monorepo; SDK is MIT. Adding an npm dependency is fine; do not vendor Legal server code into this repo.


Relevant files (bridge monorepo)

Path Why
packages/frontend/package.json Add @plasticdigits/cl8y-clickwrap (+ lockfile)
packages/frontend/src/main.tsx Provider / top-level composition if needed
packages/frontend/src/components/Layout.tsx App shell; avoid gating whole shell if connect UX must remain reachable
packages/frontend/src/components/transfer/TransferForm.tsx Primary deposit CTA / isWalletConnected; best place to enforce signed-latest for source wallet
packages/frontend/src/pages/TransferStatusPage.tsx Destination submit / execute paths that require connected wallet
packages/frontend/src/hooks/useWallet.ts Terra account
packages/frontend/src/components/ConnectWallet.tsx EVM account UI
packages/frontend/src/stores/solanaWallet.ts (and Solana connect hooks) Solana account
packages/frontend/src/utils/validateEnv.ts / .env.example Optional public overrides for API/portal base URLs in non-prod
packages/frontend/e2e/*.spec.ts Extend Playwright coverage (esp. wallet + transfer flows)
docs/FRONTEND_BRIDGE_INVARIANTS.md Add a short INV-FE-CLICKWRAP-1 (or similar) documenting the gate
packages/frontend/README.md / docs/frontend.md Integrator/dev notes for local Legal CORS + property

Upstream coordination (Legal repo — track as checklist, not necessarily implemented here):

Item Owner
Register property bridge.cl8y.com Legal ops (scripts/register-property.sh)
Add https://bridge.cl8y.com to API CORS_ORIGINS Legal ops
Add https://bridge.cl8y.com to portal VITE_REDIRECT_URI_ALLOWLIST and redeploy web Legal ops
Publish @plasticdigits/cl8y-clickwrap to npm (or approve tarball pin) Legal eng
Confirm Solana sign route production-ready Legal eng

  1. Dependency: Add @plasticdigits/cl8y-clickwrap to packages/frontend. Share one createClient() instance (module singleton or React context) with production defaults.
  2. Helper: Small hook e.g. useBridgeClickwrapAccount() that, given current source chain type (or the wallet required for the active mutative action), returns { network, account } from the correct wallet store.
  3. Gate placement (preferred):
    • On TransferForm, when isWalletConnected and account known: wrap deposit CTA / form action region with TermsGate (or useSignatureStatus + bridge-styled unsigned panel that calls the portal).
    • On TransferStatusPage, gate withdraw-submit / execute controls the same way for the wallet that must sign.
    • Keep NavBar + wallet connect modals usable while unsigned.
  4. Unsigned UX: Use SDK default gate or a bridge-styled unsigned slot that shows version label / effective date (from getTermsLatest) and a primary “Accept terms” button that redirects via buildSignUrl / TermsGate accept handler. appName e.g. "CL8Y Bridge". redirectUri = current page URL so users return to the same transfer/status deep link.
  5. After return: Rely on SDK focus re-poll (useSignatureStatus); ensure deposit CTA enables only when signed_latest === true.
  6. Docs: Add INV-FE-CLICKWRAP-1 to docs/FRONTEND_BRIDGE_INVARIANTS.md and a short agent/dev note if the repo pattern warrants a skill under skills/.
  7. Staging / local: Document that local dev needs Legal CORS + allowlist (or mocked status in unit tests). Prefer MSW / mocked ClickwrapClient in Vitest; use real Legal staging only in optional manual QA.

Acceptance criteria

  • @plasticdigits/cl8y-clickwrap is a declared dependency of packages/frontend and builds cleanly (tsc + vite build).
  • Production property used for all status/sign calls is bridge.cl8y.com.
  • With an EVM wallet connected as source and signed_latest === false, deposit/bridge CTA is blocked and user can navigate to the Legal portal to sign; after successful sign + return, status re-checks and CTA unblocks without full manual cache clear.
  • Same gating behavior for Terra Classic source wallet (Network: TerraClassic).
  • Same gating behavior for Solana source wallet (Network: Solana), or an explicit documented interim policy if Solana portal is not ready (must not silently skip).
  • Mutative status-page actions that require a connected wallet are likewise gated.
  • Disconnected users can still connect a wallet; gating starts once the relevant account is available.
  • Status/API failure fails closed for mutative actions (visible error, no silent bypass).
  • No dangerouslySetInnerHTML for Legal content; no open redirect via attacker-controlled redirect_uri constructed by the bridge.
  • Legal ops prerequisites completed or blocked with an explicit MR dependency note: property registered, CORS, redirect allowlist, package publish.
  • docs/FRONTEND_BRIDGE_INVARIANTS.md documents the clickwrap invariant; frontend README mentions the integration.
  • Unit + Playwright coverage for happy path and failure path (see test plans).
  • Existing frontend unit/e2e suites remain green (make test-frontend / package scripts).

Test plan — functional paths

Unit / component (Vitest + Testing Library)

  • Mock ClickwrapClient.getSignatureStatus → signed_latest: true → children / deposit affordance render.
  • Mock signed_latest: false → unsigned / accept UI; children mutative controls hidden or disabled.
  • Mock status throw / network error → alert / error UI; mutative controls not enabled.
  • Network selection: EVM address → EVM; Terra address → TerraClassic; Solana address → Solana.
  • No account → gate does not force accept UI that blocks wallet connect.
  • Accept / redirect builds portal URL with property=bridge.cl8y.com, correct network sign path, redirect_uri (current origin), and app_name.
  • Focus/re-poll: after simulated focus event, status refetch updates gate from unsigned → signed.

Integration / manual QA

  • Local or staging: connect MetaMask on BSC/opBNB/MegaETH source → unsigned → portal → sign → return → deposit enabled.
  • Terra Classic (Keplr) source path same.
  • Solana source path same (when portal ready).
  • Deep link return: start from /transfer/:xchainHashId or /, ensure redirect_uri restores that URL.
  • Terms version bump (Legal staging): previously signed account becomes unsigned; bridge re-gates.
  • VITE_UNDER_CONSTRUCTION=true unchanged (no clickwrap requirement on that page).

Playwright (packages/frontend/e2e)

  • New or extended spec: mock Legal API (route interception) for signed / unsigned / error; assert CTA gating.
  • Wallet-connect related specs still pass with mocks defaulting to signed_latest: true so existing transfer e2e is not flaky on external Legal uptime.
  • Optional headed smoke against staging Legal (manual or tagged @legal) — not required in default CI if external dependency is unstable.

Test plan — attack, hack & abuse vectors

Vector Expected defense How to test
Skip UI gate via DevTools (remove overlay / enable button) Bridge UI is UX only; Legal acceptance is still required for compliance records. Prefer server-side/product policy: no alternative “accept” that writes a fake localStorage flag treated as signed. Document that bypassing UI does not create a Legal signature. Attempt localStorage/session spoof; reload; status must still come from API
Spoof signed_latest via service worker / fake response in victim browser Attacker who already controls the page can do anything; out of scope beyond not trusting query-string signed=1 Ensure bridge does not read acceptance from URL/localStorage as source of truth
Open redirect: craft malicious redirect_uri when building sign URL Bridge only passes same-origin window.location.href (or fixed allowlisted path); portal allowlist rejects others Unit-test builder; manually pass https://evil.example in a patched build and confirm portal does not navigate
Open redirect / XSS via app_name or property query reflection on portal Portal responsibility (textContent); bridge passes constant app name + fixed property Do not pass user-controlled strings into appName / property
CSRF / login CSRF style: trick user to sign for bridge while intending another site Portal shows property + terms; bridge uses fixed bridge.cl8y.com Visual/manual: portal must display property bridge.cl8y.com
Phishing clone of bridge that points SDK at attacker API Pin default SDK bases; if env override exists, only in deploy config review Grep for apiBaseUrl / termsBaseUrl overrides in MR
CORS misconfiguration allowing arbitrary origins on Legal API Legal ops; bridge MR should not request * Checklist with Legal
Clickjacking bridge accept CTA Existing bridge framing policy / portal X-Frame options are Legal’s concern; bridge should not embed portal in iframe Confirm redirect (top navigation), not iframe embed
Race: status cached signed while terms version bumped mid-session Focus re-poll + poll before enabling mutative CTA; optional re-check immediately before deposit click Mock bump between polls
Account switch: signed address A, switch wallet to unsigned B Gate must key off current account; B blocked until sign Switch account in wallet mock; expect re-gate
Wrong network signature reuse (EVM signature counted for Terra) API keys by network; SDK passes correct Network Assert Terra account does not use EVM status
Property confusion (cl8y.com vs bridge.cl8y.com) Hard-code / constant property bridge.cl8y.com Unit assert property string; manual API status for both properties differs
Downgrade / mix content: http:// API in prod Default HTTPS SDK bases; ban insecure prod overrides Env review
Dependency confusion / typosquat package Exact scope @plasticdigits/cl8y-clickwrap; verify publish integrity npm ls, lockfile pin
Abuse: spam Legal status endpoint from bridge poll loops SDK should not refetch-storm on every render; bridge must not add aggressive custom polling beyond SDK Profile network tab; assert no request storm on checkbox-less re-renders
Bypass by choosing destination-only wallet without signing source Gate the wallet that performs the mutative action (source deposit and dest execute separately as applicable) E2E matrix per direction

Verification criteria

Merge is verified when all of the following hold:

  1. Build: packages/frontend typechecks and production-builds with the new dependency.
  2. Automated: New unit tests for gate/network/error paths pass; Playwright suite passes in CI with Legal API mocked to signed-by-default for legacy specs.
  3. Manual staging (or production dry-run with a test account): For at least EVM and Terra Classic source flows — unsigned → portal → sign → return → mutative CTA enabled; API failure shows error and blocks CTA.
  4. Ops: Legal confirms property bridge.cl8y.com registered; CORS + redirect allowlist deployed; npm package published (or approved alternate install documented).
  5. Docs: INV + README updated; MR description links this issue and Legal coordination checklist.
  6. No regression: Spot-check recipient validation, CTA disable rules (INV-UX1), and one happy-path transfer e2e still green.

Out of scope

  • Reimplementing the Legal portal sign pages inside the bridge
  • Telegram network clickwrap (bridge has no Telegram wallet path)
  • Changing Legal API crypto / message formats
  • Using clickwrap as on-chain bridge authorization (contracts remain unchanged)
  • i18n of terms text

References

## Summary Integrate `@plasticdigits/cl8y-clickwrap` into `packages/frontend` so wallet-connected users on **https://bridge.cl8y.com** must accept the latest CL8Y Terms & Conditions (via the hosted portal at **https://terms.cl8y.com**) before they can use transfer / deposit / withdraw UI. Bundle dependency install, multi-network gating (EVM / Terra Classic / Solana), UX wiring, ops prerequisites, docs/invariants, and tests into this single issue. **Upstream SDK / portal:** [cl8y-ecosystem-legal](https://gitlab.com/plasticdigits/cl8y-ecosystem-legal) — package `packages/cl8y-clickwrap`, API `https://api.terms.cl8y.com`, portal `https://terms.cl8y.com`. --- ## Current codebase The bridge frontend (`packages/frontend`) is a Vite + React 18 app with: | Area | Behavior today | |------|----------------| | Entry / routing | `src/main.tsx` — `BrowserRouter` + `Layout` outlet; routes `/`, `/transfer/:xchainHashId`, `/history`, `/verify`, `/settings` | | Wallets | **EVM:** wagmi (`useAccount`) via `ConnectWallet` / `EvmWalletModal`. **Terra Classic:** `useWallet` / `stores/wallet`. **Solana:** `useSolanaWallet` / `stores/solanaWallet` | | Transfer gate today | `TransferForm` already requires the **source-chain** wallet (`isWalletConnected` ≈ Terra / Solana / EVM by `direction`) before deposit CTAs — but there is **no** legal terms / signature-status check | | Legal integration | **None.** No dependency on `@plasticdigits/cl8y-clickwrap`, no calls to `api.terms.cl8y.com`, no redirect to `terms.cl8y.com` | | Production host | **https://bridge.cl8y.com** (see e.g. closed issue #119 walkthrough notes) | | Hardening culture | Frontend invariants in `docs/FRONTEND_BRIDGE_INVARIANTS.md`; Playwright under `packages/frontend/e2e/`; Vitest unit/integration under `src/**` | CL8Y Legal model (upstream): - Acceptance is **per property** (hostname). Signing for `cl8y.com` does **not** satisfy `bridge.cl8y.com`. - One signature row per `(property, terms version, network, account)`. - Integrators check `GET /api/v1/signatures/status` (SDK: `client.getSignatureStatus`), then redirect unsigned users to the portal sign URL for that network. - Portal shows full terms + scroll + consent checkbox **before** wallet connect (upstream portal invariant; do not reimplement on the bridge). SDK surface (intended): ```ts import { createClient } from "@plasticdigits/cl8y-clickwrap"; import { TermsGate } from "@plasticdigits/cl8y-clickwrap/react"; ``` `TermsGate` polls signature status, shows accept UI when `signed_latest` is false, redirects to the portal with `redirect_uri` + `app_name`, and re-checks on window focus after return. **Package availability note:** As of this issue, `@plasticdigits/cl8y-clickwrap@0.1.0` may not yet be on the public npm registry. Prefer published npm once available; until then, coordinate with Legal to publish, or temporarily install from the Legal repo tarball / git path (document the chosen path in the MR). --- ## Why this is needed 1. **Legal / product requirement:** Bridge users move real value across EVM, Terra Classic, and Solana. They must have an auditable, wallet-signed acceptance of the latest global T&Cs scoped to the bridge property before using the dapp. 2. **Consistency with CL8Y ecosystem:** Other CL8Y sites are expected to use the same Legal API + clickwrap SDK rather than one-off checkboxes that are not cryptographically bound. 3. **Version bumps:** When Legal publishes a new terms version, `signed_latest` becomes false and the bridge must re-gate until the user re-signs — without a custom versioning system in this monorepo. 4. **Do not DIY the portal:** Full terms disclosure, Content-SHA256 message binding, and redirect allowlisting already live in Legal. The bridge should **integrate**, not fork, that UX. --- ## Constraints & guardrails 1. **Property hostname:** Register and use property **`bridge.cl8y.com`** (exact production host). Do not reuse `cl8y.com` acceptance. Confirm with Legal ops before merge. 2. **CORS:** Legal API `CORS_ORIGINS` must include `https://bridge.cl8y.com` (and local Vite origin for dev, e.g. `http://localhost:3000`). Browser status checks will fail closed without this. 3. **Portal `redirect_uri` allowlist:** Legal web `VITE_REDIRECT_URI_ALLOWLIST` must include `https://bridge.cl8y.com` so post-sign navigation returns to the bridge. Localhost only when Legal enables `VITE_ALLOW_LOCALHOST_REDIRECT`. 4. **Prefer SDK, do not reimplement:** Use `@plasticdigits/cl8y-clickwrap` (`createClient`, `TermsGate` and/or `useSignatureStatus` + `buildSignUrl`). Do not copy portal sign-page DOM, do not invent a parallel local “I agree” checkbox that claims to satisfy Legal, and do not submit wallet signatures from the bridge unless Legal explicitly documents that path for integrators. 5. **Network mapping must match source wallet** (same idea as `TransferForm`’s `isWalletConnected`): | Source chain type | SDK `Network` | Account source | |-------------------|---------------|----------------| | EVM | `EVM` | wagmi `address` | | Terra Classic / cosmos | `TerraClassic` | Terra wallet store `address` | | Solana | `Solana` | Solana wallet store `address` | 6. **No account → no hard block of connect UX:** When no relevant wallet is connected, users must still be able to open wallet modals / browse read-only surfaces. Gate **mutative** bridge actions (deposit CTA, and any withdraw/execute paths that require the connected account) — not the entire app shell — unless product explicitly decides otherwise. Recommended default: wrap transfer action surface (and equivalent status-page submit/execute panels) so unsigned users see the TermsGate / accept CTA instead of deposit controls. 7. **UI consent is not auth:** UI gate + portal redirect prove UX intent; server-side Legal still requires a valid wallet signature. Do not treat a local checkbox alone as acceptance. 8. **Safe rendering:** Never `dangerouslySetInnerHTML` for terms API content or query params. Prefer portal for full text; if linking to `GET .../terms/latest/content`, open as plain text / external link. 9. **Redirect safety:** Pass `redirectUri={window.location.href}` (or a same-origin path) only. Optionally preflight with SDK `isAllowedRedirectUri` for fail-fast UX; portal remains the enforcer. 10. **Fail closed on status errors for mutative actions:** If signature status cannot be verified (network/CORS/5xx), do **not** silently enable Bridge / deposit / execute. Show a clear error + retry. 11. **Do not break existing frontend invariants** in `docs/FRONTEND_BRIDGE_INVARIANTS.md` (recipient validation INV-RCP1, CTA validity INV-UX1, rate-limit UX, MegaETH chip, token logos, etc.). 12. **Under-construction mode:** When `VITE_UNDER_CONSTRUCTION === 'true'`, clickwrap is N/A (only `UnderConstructionPage`). 13. **Secrets:** No new secret `VITE_*` keys. Public API base URLs only if overridden; default SDK bases (`api.terms.cl8y.com` / `terms.cl8y.com`) are fine for production. 14. **Solana portal readiness:** Confirm Legal portal `/sign/solana` is live for production before requiring Solana source users to complete the redirect loop. If Solana portal is not ready, document a temporary product decision in the MR (block Solana deposits with explicit message vs delay Solana gating) — do not silently skip legal for Solana while gating EVM/Terra. 15. **AGPL / npm license:** Bridge frontend is part of an AGPL monorepo; SDK is MIT. Adding an npm dependency is fine; do not vendor Legal server code into this repo. --- ## Relevant files (bridge monorepo) | Path | Why | |------|-----| | `packages/frontend/package.json` | Add `@plasticdigits/cl8y-clickwrap` (+ lockfile) | | `packages/frontend/src/main.tsx` | Provider / top-level composition if needed | | `packages/frontend/src/components/Layout.tsx` | App shell; avoid gating whole shell if connect UX must remain reachable | | `packages/frontend/src/components/transfer/TransferForm.tsx` | Primary deposit CTA / `isWalletConnected`; best place to enforce signed-latest for source wallet | | `packages/frontend/src/pages/TransferStatusPage.tsx` | Destination submit / execute paths that require connected wallet | | `packages/frontend/src/hooks/useWallet.ts` | Terra account | | `packages/frontend/src/components/ConnectWallet.tsx` | EVM account UI | | `packages/frontend/src/stores/solanaWallet.ts` (and Solana connect hooks) | Solana account | | `packages/frontend/src/utils/validateEnv.ts` / `.env.example` | Optional public overrides for API/portal base URLs in non-prod | | `packages/frontend/e2e/*.spec.ts` | Extend Playwright coverage (esp. wallet + transfer flows) | | `docs/FRONTEND_BRIDGE_INVARIANTS.md` | Add a short **INV-FE-CLICKWRAP-1** (or similar) documenting the gate | | `packages/frontend/README.md` / `docs/frontend.md` | Integrator/dev notes for local Legal CORS + property | **Upstream coordination (Legal repo — track as checklist, not necessarily implemented here):** | Item | Owner | |------|--------| | Register property `bridge.cl8y.com` | Legal ops (`scripts/register-property.sh`) | | Add `https://bridge.cl8y.com` to API `CORS_ORIGINS` | Legal ops | | Add `https://bridge.cl8y.com` to portal `VITE_REDIRECT_URI_ALLOWLIST` and redeploy web | Legal ops | | Publish `@plasticdigits/cl8y-clickwrap` to npm (or approve tarball pin) | Legal eng | | Confirm Solana sign route production-ready | Legal eng | --- ## Recommended solution direction 1. **Dependency:** Add `@plasticdigits/cl8y-clickwrap` to `packages/frontend`. Share one `createClient()` instance (module singleton or React context) with production defaults. 2. **Helper:** Small hook e.g. `useBridgeClickwrapAccount()` that, given current **source** chain type (or the wallet required for the active mutative action), returns `{ network, account }` from the correct wallet store. 3. **Gate placement (preferred):** - On `TransferForm`, when `isWalletConnected` and account known: wrap deposit CTA / form action region with `TermsGate` (or `useSignatureStatus` + bridge-styled unsigned panel that calls the portal). - On `TransferStatusPage`, gate withdraw-submit / execute controls the same way for the wallet that must sign. - Keep NavBar + wallet connect modals usable while unsigned. 4. **Unsigned UX:** Use SDK default gate or a bridge-styled `unsigned` slot that shows version label / effective date (from `getTermsLatest`) and a primary “Accept terms” button that redirects via `buildSignUrl` / `TermsGate` accept handler. `appName` e.g. `"CL8Y Bridge"`. `redirectUri` = current page URL so users return to the same transfer/status deep link. 5. **After return:** Rely on SDK focus re-poll (`useSignatureStatus`); ensure deposit CTA enables only when `signed_latest === true`. 6. **Docs:** Add **INV-FE-CLICKWRAP-1** to `docs/FRONTEND_BRIDGE_INVARIANTS.md` and a short agent/dev note if the repo pattern warrants a skill under `skills/`. 7. **Staging / local:** Document that local dev needs Legal CORS + allowlist (or mocked status in unit tests). Prefer MSW / mocked `ClickwrapClient` in Vitest; use real Legal staging only in optional manual QA. --- ## Acceptance criteria - [ ] `@plasticdigits/cl8y-clickwrap` is a declared dependency of `packages/frontend` and builds cleanly (`tsc` + `vite build`). - [ ] Production property used for all status/sign calls is **`bridge.cl8y.com`**. - [ ] With an EVM wallet connected as source and `signed_latest === false`, deposit/bridge CTA is blocked and user can navigate to the Legal portal to sign; after successful sign + return, status re-checks and CTA unblocks without full manual cache clear. - [ ] Same gating behavior for Terra Classic source wallet (`Network: TerraClassic`). - [ ] Same gating behavior for Solana source wallet (`Network: Solana`), **or** an explicit documented interim policy if Solana portal is not ready (must not silently skip). - [ ] Mutative status-page actions that require a connected wallet are likewise gated. - [ ] Disconnected users can still connect a wallet; gating starts once the relevant account is available. - [ ] Status/API failure fails closed for mutative actions (visible error, no silent bypass). - [ ] No `dangerouslySetInnerHTML` for Legal content; no open redirect via attacker-controlled `redirect_uri` constructed by the bridge. - [ ] Legal ops prerequisites completed or blocked with an explicit MR dependency note: property registered, CORS, redirect allowlist, package publish. - [ ] `docs/FRONTEND_BRIDGE_INVARIANTS.md` documents the clickwrap invariant; frontend README mentions the integration. - [ ] Unit + Playwright coverage for happy path and failure path (see test plans). - [ ] Existing frontend unit/e2e suites remain green (`make test-frontend` / package scripts). --- ## Test plan — functional paths ### Unit / component (Vitest + Testing Library) - [ ] Mock `ClickwrapClient.getSignatureStatus` → `signed_latest: true` → children / deposit affordance render. - [ ] Mock `signed_latest: false` → unsigned / accept UI; children mutative controls hidden or disabled. - [ ] Mock status throw / network error → alert / error UI; mutative controls not enabled. - [ ] Network selection: EVM address → `EVM`; Terra address → `TerraClassic`; Solana address → `Solana`. - [ ] No account → gate does not force accept UI that blocks wallet connect. - [ ] Accept / redirect builds portal URL with `property=bridge.cl8y.com`, correct network sign path, `redirect_uri` (current origin), and `app_name`. - [ ] Focus/re-poll: after simulated focus event, status refetch updates gate from unsigned → signed. ### Integration / manual QA - [ ] Local or staging: connect MetaMask on BSC/opBNB/MegaETH source → unsigned → portal → sign → return → deposit enabled. - [ ] Terra Classic (Keplr) source path same. - [ ] Solana source path same (when portal ready). - [ ] Deep link return: start from `/transfer/:xchainHashId` or `/`, ensure `redirect_uri` restores that URL. - [ ] Terms version bump (Legal staging): previously signed account becomes unsigned; bridge re-gates. - [ ] `VITE_UNDER_CONSTRUCTION=true` unchanged (no clickwrap requirement on that page). ### Playwright (`packages/frontend/e2e`) - [ ] New or extended spec: mock Legal API (route interception) for signed / unsigned / error; assert CTA gating. - [ ] Wallet-connect related specs still pass with mocks defaulting to `signed_latest: true` so existing transfer e2e is not flaky on external Legal uptime. - [ ] Optional headed smoke against staging Legal (manual or tagged `@legal`) — not required in default CI if external dependency is unstable. --- ## Test plan — attack, hack & abuse vectors | Vector | Expected defense | How to test | |--------|------------------|-------------| | Skip UI gate via DevTools (remove overlay / enable button) | Bridge UI is UX only; Legal acceptance is still required for compliance records. Prefer server-side/product policy: no alternative “accept” that writes a fake localStorage flag treated as signed. Document that bypassing UI does not create a Legal signature. | Attempt localStorage/session spoof; reload; status must still come from API | | Spoof `signed_latest` via service worker / fake response in victim browser | Attacker who already controls the page can do anything; out of scope beyond not trusting query-string `signed=1` | Ensure bridge does not read acceptance from URL/localStorage as source of truth | | Open redirect: craft malicious `redirect_uri` when building sign URL | Bridge only passes same-origin `window.location.href` (or fixed allowlisted path); portal allowlist rejects others | Unit-test builder; manually pass `https://evil.example` in a patched build and confirm portal does not navigate | | Open redirect / XSS via `app_name` or `property` query reflection on portal | Portal responsibility (textContent); bridge passes constant app name + fixed property | Do not pass user-controlled strings into `appName` / `property` | | CSRF / login CSRF style: trick user to sign for bridge while intending another site | Portal shows property + terms; bridge uses fixed `bridge.cl8y.com` | Visual/manual: portal must display property `bridge.cl8y.com` | | Phishing clone of bridge that points SDK at attacker API | Pin default SDK bases; if env override exists, only in deploy config review | Grep for `apiBaseUrl` / `termsBaseUrl` overrides in MR | | CORS misconfiguration allowing arbitrary origins on Legal API | Legal ops; bridge MR should not request `*` | Checklist with Legal | | Clickjacking bridge accept CTA | Existing bridge framing policy / portal X-Frame options are Legal’s concern; bridge should not embed portal in iframe | Confirm redirect (top navigation), not iframe embed | | Race: status cached signed while terms version bumped mid-session | Focus re-poll + poll before enabling mutative CTA; optional re-check immediately before deposit click | Mock bump between polls | | Account switch: signed address A, switch wallet to unsigned B | Gate must key off **current** account; B blocked until sign | Switch account in wallet mock; expect re-gate | | Wrong network signature reuse (EVM signature counted for Terra) | API keys by network; SDK passes correct `Network` | Assert Terra account does not use `EVM` status | | Property confusion (`cl8y.com` vs `bridge.cl8y.com`) | Hard-code / constant property `bridge.cl8y.com` | Unit assert property string; manual API status for both properties differs | | Downgrade / mix content: `http://` API in prod | Default HTTPS SDK bases; ban insecure prod overrides | Env review | | Dependency confusion / typosquat package | Exact scope `@plasticdigits/cl8y-clickwrap`; verify publish integrity | `npm ls`, lockfile pin | | Abuse: spam Legal status endpoint from bridge poll loops | SDK should not refetch-storm on every render; bridge must not add aggressive custom polling beyond SDK | Profile network tab; assert no request storm on checkbox-less re-renders | | Bypass by choosing destination-only wallet without signing source | Gate the wallet that performs the mutative action (source deposit and dest execute separately as applicable) | E2E matrix per direction | --- ## Verification criteria Merge is verified when **all** of the following hold: 1. **Build:** `packages/frontend` typechecks and production-builds with the new dependency. 2. **Automated:** New unit tests for gate/network/error paths pass; Playwright suite passes in CI with Legal API mocked to signed-by-default for legacy specs. 3. **Manual staging (or production dry-run with a test account):** For at least EVM and Terra Classic source flows — unsigned → portal → sign → return → mutative CTA enabled; API failure shows error and blocks CTA. 4. **Ops:** Legal confirms property `bridge.cl8y.com` registered; CORS + redirect allowlist deployed; npm package published (or approved alternate install documented). 5. **Docs:** INV + README updated; MR description links this issue and Legal coordination checklist. 6. **No regression:** Spot-check recipient validation, CTA disable rules (INV-UX1), and one happy-path transfer e2e still green. --- ## Out of scope - Reimplementing the Legal portal sign pages inside the bridge - Telegram network clickwrap (bridge has no Telegram wallet path) - Changing Legal API crypto / message formats - Using clickwrap as on-chain bridge authorization (contracts remain unchanged) - i18n of terms text ## References - Legal repo README + `packages/cl8y-clickwrap/README.md` - Legal skills: `portal-sign-disclosure`, `security-ops` - Production bridge: https://bridge.cl8y.com/ - Production Legal: https://terms.cl8y.com/ · https://api.terms.cl8y.com/
PlasticDigits commented 2026-08-18 00:20:54 +00:00 (Migrated from gitlab.com)

mentioned in issue #137

mentioned in issue #137
PlasticDigits commented 2026-08-18 00:20:55 +00:00 (Migrated from gitlab.com)

marked as related to #137

marked as related to #137
PlasticDigits commented 2026-08-31 05:55:23 +00:00 (Migrated from gitlab.com)

mentioned in commit 3553667f0e

mentioned in commit 3553667f0eb29361ba92a881f9483cc6f06188a4
PlasticDigits commented 2026-08-31 05:55:47 +00:00 (Migrated from gitlab.com)

mentioned in merge request !156

mentioned in merge request !156
PlasticDigits commented 2026-08-31 09:14:53 +00:00 (Migrated from gitlab.com)

mentioned in merge request !159

mentioned in merge request !159
PlasticDigits commented 2026-08-31 11:36:53 +00:00 (Migrated from gitlab.com)

mentioned in commit 9bc7d3951a

mentioned in commit 9bc7d3951ab6605eda5867ecda02cbb397d3a8b8
PlasticDigits commented 2026-08-31 12:12:02 +00:00 (Migrated from gitlab.com)

mentioned in commit a5a7620ccf

mentioned in commit a5a7620ccfe4065ae83abb79d7bef96893dbb776
PlasticDigits commented 2026-08-31 12:39:02 +00:00 (Migrated from gitlab.com)

Merge status (!156 landed on main)

Code from !156 is merged. Do not close this issue yet — production gating is still blocked outside this repo.

Remaining (issue AC / verification)

  • Legal ops: register property bridge.cl8y.com
  • API CORS_ORIGINS includes https://bridge.cl8y.com (and http://localhost:3000 for local)
  • Portal VITE_REDIRECT_URI_ALLOWLIST includes https://bridge.cl8y.com
  • Confirm Solana /sign/solana production-ready (envelope vs API verify). Solana stays fail-closed until signed_latest — do not skip.
  • Manual portal round-trip: unsigned EVM/Terra → terms.cl8y.com → return → deposit CTA
  • Combined stacking with !159 / GL-137: header Connect still tappable while unsigned; Connect tap ≠ skip T&C
  • Do not deploy this SPA to https://bridge.cl8y.com until the Legal ops boxes are confirmed. Fail-closed code will disable production deposits without them.

Landed in merge

SDK @plasticdigits/cl8y-clickwrap@0.1.1, property bridge.cl8y.com, mutative CTA gate, fail-closed on API error, header Connect outside TermsGate.

Post-merge: e2e specs that still used accessible name CONNECT TC were updated to the GL-137 header locator (Connect Terra Wallet / data-testid=connect-terra-wallet).

## Merge status (!156 landed on `main`) Code from !156 is merged. **Do not close this issue yet** — production gating is still blocked outside this repo. ### Remaining (issue AC / verification) - [ ] Legal ops: register property `bridge.cl8y.com` - [ ] API `CORS_ORIGINS` includes `https://bridge.cl8y.com` (and `http://localhost:3000` for local) - [ ] Portal `VITE_REDIRECT_URI_ALLOWLIST` includes `https://bridge.cl8y.com` - [ ] Confirm Solana `/sign/solana` production-ready (envelope vs API verify). Solana stays fail-closed until `signed_latest` — do not skip. - [ ] Manual portal round-trip: unsigned EVM/Terra → `terms.cl8y.com` → return → deposit CTA - [ ] Combined stacking with !159 / GL-137: header Connect still tappable while unsigned; Connect tap ≠ skip T&C - [ ] Do **not** deploy this SPA to https://bridge.cl8y.com until the Legal ops boxes are confirmed. Fail-closed code will disable production deposits without them. ### Landed in merge SDK `@plasticdigits/cl8y-clickwrap@0.1.1`, property `bridge.cl8y.com`, mutative CTA gate, fail-closed on API error, header Connect outside TermsGate. Post-merge: e2e specs that still used accessible name `CONNECT TC` were updated to the GL-137 header locator (`Connect Terra Wallet` / `data-testid=connect-terra-wallet`).
PlasticDigits commented 2026-08-31 12:54:22 +00:00 (Migrated from gitlab.com)

Post-merge e2e follow-up on main (a211a73): clickwrap/validation/dev-wallet specs now use the GL-137 header locator (headerTerraConnect / Connect Terra Wallet). Playwright e2e/wallet-connect.spec.ts + e2e/clickwrap.spec.ts passed locally with 5 workers (12/12). Legal ops boxes above still block production deploy.

Post-merge e2e follow-up on `main` (`a211a73`): clickwrap/validation/dev-wallet specs now use the GL-137 header locator (`headerTerraConnect` / `Connect Terra Wallet`). Playwright `e2e/wallet-connect.spec.ts` + `e2e/clickwrap.spec.ts` passed locally with 5 workers (12/12). Legal ops boxes above still block production deploy.
Member

/agent implement

/agent implement

cl8y-agent-control: queued implement job a090f0ff-0711-4f5b-8561-7a05a52c2c9f (not executed; no Hetzner VM).

cl8y-agent-control: queued `implement` job `a090f0ff-0711-4f5b-8561-7a05a52c2c9f` (not executed; no Hetzner VM).
Sign in to join this conversation.
No milestone
No project
No assignees
3 participants
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#134
No description provided.