feat(frontend): add CL8Y Legal clickwrap (Terra Classic TermsGate) #12

Closed
opened 2026-08-15 09:36:28 +00:00 by PlasticDigits · 4 comments
PlasticDigits commented 2026-08-15 09:36:28 +00:00 (Migrated from gitlab.com)

Summary

Integrate @plasticdigits/cl8y-clickwrap from cl8y-ecosystem-legal so connected Terra Classic wallets must accept the latest CL8Y ecosystem Terms & Conditions for this site’s property before swap / referral-register (and any other wallet execute). Signing happens on the hosted portal (https://terms.cl8y.com); this app only checks status and redirects.

Reference implementation (do not copy DEX-only files blindly): cl8y-dex-terraclassic#517 + ConnectedTermsGate / legalClickwrap.ts / skills/AGENTS_FRONTEND_CLICKWRAP.md.


Current codebase

Component Path Behavior today
App shell frontend/src/App.tsx, components/layout/Layout.tsx Header + <main>{children}</main> + Footer. No terms gate, no first-visit NFA modal.
Wallet hooks/useWallet.ts, stores/wallet.ts, WalletButton.tsx Station / Keplr / Leap / Cosmostation / WC. address is terra1… on columbus-5. Connect does not check Legal.
Swap execute hooks/useSwap.ts → contractService.executeSwap connected is the only wallet gate. CTA: connect or swap.
Referral execute pages/ReferralPage.tsx → registerReferralCode Same: connected + USTR balance only.
Footer components/layout/Footer.tsx GitHub, Telegram, CL8Y, Docs. No Terms link.
Dependencies frontend/package.json React 18, Vite 5. No @plasticdigits/cl8y-clickwrap. No .npmrc for the GitLab npm registry.
CSP frontend/vite.config.ts No production CSP connect-src helper (unlike DEX viteCsp.ts).
Tests frontend/ No Vitest/Playwright suite today.
Prod host frontend/index.html og:url https://ust1cmm.com
Legal platform (other repo) cl8y-ecosystem-legal Per-property acceptances; Terra Classic = Keplr ADR-036 on portal; API https://api.terms.cl8y.com. Signing on cl8y.com does not satisfy ust1cmm.com.

TermsGate from the SDK hides children when account is empty (renders fallback). Wrapping the whole Layout without a disconnected passthrough would blank Home/Treasury for anonymous users.


Why this is needed

  1. USTR CMM is a live mainnet swap + treasury UI. Ecosystem T&Cs must be wallet-bound and versioned, not a footer copyright line.
  2. Acceptances are per hostname. A signature on cl8y.com or dex.cl8y.com is not evidence for ust1cmm.com.
  3. Portal + API already do disclosure, scroll+checkbox consent, Content-SHA256 message binding, and Terra Classic verify. Reimplementing ADR-036 or submitWallet here would diverge and is out of scope.
  4. Without a gate, any connected wallet can Swap or registerReferralCode with no Legal record.

Constraints / guardrails

  1. SDK only (C1). Use @plasticdigits/cl8y-clickwrap (createClient, TermsGate / useSignatureStatus, sanitizeRedirectUri). Do not implement Terra Classic ADR-036 verify or POST /signatures/wallet in this repo.
  2. Property (C2). Production property is ust1cmm.com (browser Origin). Override only via VITE_LEGAL_PROPERTY for staging. Never query cl8y.com / dex.cl8y.com by accident.
  3. Network (C3). Always network="TerraClassic" → API TERRA_CLASSIC. Never EVM / Solana / Telegram on this dapp.
  4. Disconnected browse stays open. Home, Treasury, Dashboard, Referral leaderboard remain usable without a wallet. Do not mount raw TermsGate as the app root.
  5. Fail closed after connect (C5). If status is loading, error, or signed_latest !== true, do not render swap/register execute paths (or the gated <main>). API outage ≠ signed. Do not treat localStorage as proof.
  6. Redirect safety (C6). Pass a sanitized redirect_uri (sanitizeRedirectUri + allowlist including https://ust1cmm.com). Portal VITE_REDIRECT_URI_ALLOWLIST remains authoritative. Prod builds must not send localhost redirect URIs.
  7. No iframe. Full navigation to sign_urls.terra_classic. Do not embed the portal.
  8. No Legal admin secrets in the frontend. Public terms/latest + signatures/status only.
  9. Do not cache signed_latest forever in localStorage. Rely on SDK status + window-focus re-poll. Terms version bumps must re-gate.
  10. No pollIntervalMs tight loop (default focus-only is enough). Do not hammer api.terms.cl8y.com.
  11. E2E hatch (if/when Playwright exists): VITE_PLAYWRIGHT_E2E=true may skip the gate for automation only. Must be unset on production / Coolify / manual QA.
  12. Style the unsigned panel to match this site (amber/glass). Default SDK markup is unstyled.
  13. Header + wallet stay mounted so the user can disconnect if they refuse terms.
  14. Clickwrap is legal evidence, not on-chain enforcement. Do not claim the treasury/swap contracts check T&Cs.

Relevant files

This repo (ustr-cmm)

Area Paths
Shell frontend/src/App.tsx, frontend/src/components/layout/Layout.tsx, Header.tsx, Footer.tsx
Wallet frontend/src/hooks/useWallet.ts, stores/wallet.ts, components/common/WalletButton.tsx
Tx surfaces hooks/useSwap.ts, components/swap/SwapCard.tsx, pages/ReferralPage.tsx, services/contract.ts (executeSwap, registerReferralCode)
Env / npm frontend/package.json, new frontend/.npmrc, frontend/src/vite-env.d.ts
New (recommended) frontend/src/utils/legalClickwrap.ts, frontend/src/components/legal/ConnectedTermsGate.tsx
Area Notes
SDK @plasticdigits/cl8y-clickwrap — GitLab npm project 82547916 (@plasticdigits:registry=https://gitlab.com/api/v4/projects/82547916/packages/npm/)
API https://api.terms.cl8y.com — GET /api/v1/terms/latest, GET /api/v1/signatures/status
Portal https://terms.cl8y.com/sign/terra-classic?property=ust1cmm.com
Docs Legal README integrator flow; packages/cl8y-clickwrap/README.md; skills/portal-sign-disclosure, skills/terra-classic-adr036, skills/security-ops
Ops Register property; CORS_ORIGINS; portal redirect allowlist

Mirror the DEX shell gate, adapted to this Layout (no #138 risk modal here).

  1. npm: add frontend/.npmrc scoping @plasticdigits to GitLab project 82547916. cd frontend && npm install @plasticdigits/cl8y-clickwrap.
  2. legalClickwrap.ts: singleton createClient(), DEFAULT_LEGAL_PROPERTY = 'ust1cmm.com', LEGAL_REDIRECT_ALLOWLIST = ['https://ust1cmm.com'], resolveLegalRedirectUri(), optional VITE_LEGAL_* overrides, skipLegalClickwrapForAutomation().
  3. ConnectedTermsGate: if no address (or automation skip) → children. Else wrap TermsGate with network="TerraClassic", appName="USTR CMM", sanitized redirectUri, styled fallback / unsigned.
  4. Mount around {children} inside Layout <main> (or equivalent) so Header/Footer/WalletButton stay up. Do not gate the whole page chrome.
  5. Footer: “Terms” → https://terms.cl8y.com or GET …/terms/latest/content?property=ust1cmm.com (new tab, noopener noreferrer).
  6. Ops (same issue, often a separate Legal Coolify change):
    # from cl8y-ecosystem-legal checkout — script prompts for ADMIN_TOKEN (hidden)
    ./scripts/register-property.sh ust1cmm.com "USTR CMM"
    ./scripts/register-property.sh --list
    
    Add https://ust1cmm.com to Legal API CORS_ORIGINS and portal VITE_REDIRECT_URI_ALLOWLIST; redeploy Legal API + web.
  7. Style the unsigned card to existing Card / amber tokens. Button: “Accept Terms” → portal (SDK handleAccept).
  8. Optional defense in depth: useSwap / registerReferralCode also no-op unless signed_latest (shell gate is primary).
// Sketch — disconnected users keep browse access
function ConnectedTermsGate({ children }: { children: React.ReactNode }) {
  const { address } = useWallet();
  if (!address) return <>{children}</>;
  return (
    <TermsGate
      client={getLegalClickwrapClient()}
      property={getLegalProperty()}
      network="TerraClassic"
      account={address}
      redirectUri={resolveLegalRedirectUri() ?? undefined}
      appName="USTR CMM"
    >
      {children}
    </TermsGate>
  );
}

Acceptance criteria

  • @plasticdigits/cl8y-clickwrap installed via GitLab npm; no forked Terra verify / wallet submit in this repo.
  • Property ust1cmm.com used on every status + sign URL; registered on Legal admin.
  • Disconnected: Home / Treasury / Dashboard / Referral browse work; no “Checking terms…” wall.
  • Connected + signed_latest: false (or stale version): styled accept UI; Swap Now / register-code cannot fire; Header disconnect still works.
  • Connected + signed_latest: true: existing swap/referral UX unchanged.
  • Accept → full navigation to portal /sign/terra-classic?property=ust1cmm.com with allowlisted redirect_uri + appName.
  • Return + window focus → status re-poll → children shown without a second sign if already latest.
  • Legal API error / CORS fail after connect: fail-closed message, not the swap form.
  • Footer Terms link present.
  • Legal Coolify: CORS_ORIGINS + portal redirect allowlist include https://ust1cmm.com.
  • No ADMIN_TOKEN, no localhost redirect in prod bundles, no signed_latest durable localStorage cache.

Test plan (all paths)

Disconnected / browse

  • No wallet: Treasury, Home, Dashboard, Referral list render; zero Legal status calls (or only if you prefetch — prefer zero).
  • Footer Terms opens content/portal in a new tab.

Connect + unsigned

  • Connect unsigned terra1… → one GET /signatures/status?property=ust1cmm.com&network=TERRA_CLASSIC&account=….
  • Main shows accept panel (version label + Read full terms + Accept).
  • Swap / register CTAs not executable (hidden or disabled behind the gate).
  • Accept → terms.cl8y.com/sign/terra-classic?property=ust1cmm.com&redirect_uri=https%3A%2F%2Fust1cmm.com%2F….
  • Disconnect from header while gated → browse restored.

Connect + signed

  • signed_latest: true → children immediately; swap/register work as today.
  • Terms version bump on Legal → next focus/status shows re-accept (old version noted if SDK provides signed_version).

Return from portal

  • Allowlisted redirect_uri returns to the same path (e.g. / or /referral).
  • Focus re-poll flips to signed without manual refresh.
  • Referral code in /swap/:code still in the URL / storage after round-trip.

Failure / env

  • API 5xx / network error → fail-closed copy; no swap.
  • CORS missing ust1cmm.com → fail-closed (document ops fix).
  • VITE_LEGAL_PROPERTY staging override does not leak into the production default.
  • Prod build: resolveLegalRedirectUri rejects http://localhost:5173.
  • Localhost redirect allowed only in non-PROD (and only if portal VITE_ALLOW_LOCALHOST_REDIRECT is on for that env).

Ops

  • register-property.sh ust1cmm.com listed; GET /api/v1/terms/latest?property=ust1cmm.com returns sign_urls.terra_classic.

Test plan (attack, hack, abuse)

Vector Risk Test / guard
Raw TermsGate on <App> Anonymous users see only fallback; site looks down Disconnected path renders children
Fail-open on API error Outage = unsigned users can swap Error UI; executeSwap not reachable
signed_latest in localStorage User forges a flag No durable cache; only API signed_latest
Wrong property Sign cl8y.com, use this site Status + portal always ust1cmm.com
Wrong network (EVM) Status never matches terra1 Hard-code TerraClassic
Open redirect redirect_uri=https://evil.example Client sanitize + portal allowlist; evil origin never auto-navigated
localhost redirect in prod Token/session bounce to attacker box allowLocalhost: !import.meta.env.PROD
Iframe portal Clickjacking / weakened clickwrap Full-page window.location; no iframe
Reimplement ADR-036 here Divergent verify, bad proofs Grep: no signArbitrary / submitWallet in frontend/src except SDK
ADMIN_TOKEN in Vite env Admin Legal API from the browser Grep env examples; public GETs only
CORS * or copy-paste origin Other sites read status (low) / confuse property Ops: explicit https://ust1cmm.com only
Status poll hammer Many tabs × interval vs Legal rate limits No pollIntervalMs unless ≥ focus-only; reuse singleton client
XSS → fake gate Injected “signed” UI React text nodes; do not innerHTML terms
MITM Legal API Fake signed_latest: true HTTPS defaults; do not allow user-supplied apiBaseUrl from query string
Address spoof in query Attacker checks someone else’s status (privacy) Only the connected wallet address is sent
Playwright skip in prod VITE_PLAYWRIGHT_E2E=true baked into Coolify Document: unset on ust1cmm.com builds; grep deploy env
Claim on-chain enforcement Users think contracts check T&Cs Copy: Legal acceptance is off-chain; contracts unchanged
Referral deep-link loss /swap/code stripped by portal return redirect_uri is current href including path

Verification criteria

  1. On https://ust1cmm.com with a new wallet: connect → accept panel → portal Terra Classic sign → return → one successful status signed_latest: true → swap or register works.
  2. Same wallet on a hard refresh: no portal bounce; children show.
  3. DevTools: status URL has property=ust1cmm.com and network=TERRA_CLASSIC; no Legal admin routes; no BSC/EVM sign URLs.
  4. Disconnected user can load /treasury with no Legal status request required.
  5. Legal ops: property listed; CORS + redirect allowlist contain https://ust1cmm.com (Coolify Legal API + portal rebuild).
  6. Grep clean: no ADMIN_TOKEN in frontend/; no custom ADR-036; property default is ust1cmm.com.
## Summary Integrate [`@plasticdigits/cl8y-clickwrap`](https://gitlab.com/PlasticDigits/cl8y-ecosystem-legal/-/tree/main/packages/cl8y-clickwrap) from [cl8y-ecosystem-legal](https://gitlab.com/PlasticDigits/cl8y-ecosystem-legal) so **connected Terra Classic wallets** must accept the latest CL8Y ecosystem Terms & Conditions **for this site’s property** before swap / referral-register (and any other wallet execute). Signing happens on the hosted portal (`https://terms.cl8y.com`); this app only checks status and redirects. Reference implementation (do not copy DEX-only files blindly): [cl8y-dex-terraclassic#517](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/517) + `ConnectedTermsGate` / `legalClickwrap.ts` / `skills/AGENTS_FRONTEND_CLICKWRAP.md`. --- ## Current codebase | Component | Path | Behavior today | |-----------|------|----------------| | App shell | `frontend/src/App.tsx`, `components/layout/Layout.tsx` | `Header` + `<main>{children}</main>` + `Footer`. **No** terms gate, **no** first-visit NFA modal. | | Wallet | `hooks/useWallet.ts`, `stores/wallet.ts`, `WalletButton.tsx` | Station / Keplr / Leap / Cosmostation / WC. `address` is `terra1…` on `columbus-5`. Connect does **not** check Legal. | | Swap execute | `hooks/useSwap.ts` → `contractService.executeSwap` | `connected` is the only wallet gate. CTA: connect or swap. | | Referral execute | `pages/ReferralPage.tsx` → `registerReferralCode` | Same: connected + USTR balance only. | | Footer | `components/layout/Footer.tsx` | GitHub, Telegram, CL8Y, Docs. **No Terms link.** | | Dependencies | `frontend/package.json` | React 18, Vite 5. **No** `@plasticdigits/cl8y-clickwrap`. **No** `.npmrc` for the GitLab npm registry. | | CSP | `frontend/vite.config.ts` | No production CSP `connect-src` helper (unlike DEX `viteCsp.ts`). | | Tests | `frontend/` | No Vitest/Playwright suite today. | | Prod host | `frontend/index.html` `og:url` | **`https://ust1cmm.com`** | | Legal platform (other repo) | [cl8y-ecosystem-legal](https://gitlab.com/PlasticDigits/cl8y-ecosystem-legal) | Per-**property** acceptances; Terra Classic = Keplr ADR-036 on portal; API `https://api.terms.cl8y.com`. Signing on `cl8y.com` does **not** satisfy `ust1cmm.com`. | `TermsGate` from the SDK **hides children when `account` is empty** (renders `fallback`). Wrapping the whole `Layout` without a disconnected passthrough would blank Home/Treasury for anonymous users. --- ## Why this is needed 1. USTR CMM is a live mainnet swap + treasury UI. Ecosystem T&Cs must be **wallet-bound and versioned**, not a footer copyright line. 2. Acceptances are **per hostname**. A signature on `cl8y.com` or `dex.cl8y.com` is not evidence for `ust1cmm.com`. 3. Portal + API already do disclosure, scroll+checkbox consent, `Content-SHA256` message binding, and Terra Classic verify. Reimplementing ADR-036 or `submitWallet` here would diverge and is out of scope. 4. Without a gate, any connected wallet can `Swap` or `registerReferralCode` with no Legal record. --- ## Constraints / guardrails 1. **SDK only (C1).** Use `@plasticdigits/cl8y-clickwrap` (`createClient`, `TermsGate` / `useSignatureStatus`, `sanitizeRedirectUri`). **Do not** implement Terra Classic ADR-036 verify or POST `/signatures/wallet` in this repo. 2. **Property (C2).** Production property is **`ust1cmm.com`** (browser Origin). Override only via `VITE_LEGAL_PROPERTY` for staging. Never query `cl8y.com` / `dex.cl8y.com` by accident. 3. **Network (C3).** Always `network="TerraClassic"` → API `TERRA_CLASSIC`. Never EVM / Solana / Telegram on this dapp. 4. **Disconnected browse stays open.** Home, Treasury, Dashboard, Referral leaderboard remain usable **without** a wallet. Do **not** mount raw `TermsGate` as the app root. 5. **Fail closed after connect (C5).** If status is loading, error, or `signed_latest !== true`, do **not** render swap/register execute paths (or the gated `<main>`). API outage ≠ signed. Do not treat localStorage as proof. 6. **Redirect safety (C6).** Pass a sanitized `redirect_uri` (`sanitizeRedirectUri` + allowlist including `https://ust1cmm.com`). Portal `VITE_REDIRECT_URI_ALLOWLIST` remains authoritative. Prod builds must **not** send `localhost` redirect URIs. 7. **No iframe.** Full navigation to `sign_urls.terra_classic`. Do not embed the portal. 8. **No Legal admin secrets** in the frontend. Public `terms/latest` + `signatures/status` only. 9. **Do not cache `signed_latest` forever** in localStorage. Rely on SDK status + window-focus re-poll. Terms version bumps must re-gate. 10. **No `pollIntervalMs` tight loop** (default focus-only is enough). Do not hammer `api.terms.cl8y.com`. 11. **E2E hatch (if/when Playwright exists):** `VITE_PLAYWRIGHT_E2E=true` may skip the gate for automation only. Must be unset on production / Coolify / manual QA. 12. **Style the unsigned panel** to match this site (amber/glass). Default SDK markup is unstyled. 13. **Header + wallet stay mounted** so the user can disconnect if they refuse terms. 14. Clickwrap is **legal evidence, not on-chain enforcement**. Do not claim the treasury/swap contracts check T&Cs. --- ## Relevant files ### This repo (`ustr-cmm`) | Area | Paths | |------|--------| | Shell | `frontend/src/App.tsx`, `frontend/src/components/layout/Layout.tsx`, `Header.tsx`, `Footer.tsx` | | Wallet | `frontend/src/hooks/useWallet.ts`, `stores/wallet.ts`, `components/common/WalletButton.tsx` | | Tx surfaces | `hooks/useSwap.ts`, `components/swap/SwapCard.tsx`, `pages/ReferralPage.tsx`, `services/contract.ts` (`executeSwap`, `registerReferralCode`) | | Env / npm | `frontend/package.json`, new `frontend/.npmrc`, `frontend/src/vite-env.d.ts` | | New (recommended) | `frontend/src/utils/legalClickwrap.ts`, `frontend/src/components/legal/ConnectedTermsGate.tsx` | ### Legal platform (ops / cross-repo) | Area | Notes | |------|--------| | SDK | `@plasticdigits/cl8y-clickwrap` — GitLab npm project **82547916** (`@plasticdigits:registry=https://gitlab.com/api/v4/projects/82547916/packages/npm/`) | | API | `https://api.terms.cl8y.com` — `GET /api/v1/terms/latest`, `GET /api/v1/signatures/status` | | Portal | `https://terms.cl8y.com/sign/terra-classic?property=ust1cmm.com` | | Docs | Legal README integrator flow; `packages/cl8y-clickwrap/README.md`; `skills/portal-sign-disclosure`, `skills/terra-classic-adr036`, `skills/security-ops` | | Ops | Register property; `CORS_ORIGINS`; portal redirect allowlist | --- ## Recommended direction Mirror the DEX shell gate, adapted to this Layout (no `#138` risk modal here). 1. **npm:** add `frontend/.npmrc` scoping `@plasticdigits` to GitLab project `82547916`. `cd frontend && npm install @plasticdigits/cl8y-clickwrap`. 2. **`legalClickwrap.ts`:** singleton `createClient()`, `DEFAULT_LEGAL_PROPERTY = 'ust1cmm.com'`, `LEGAL_REDIRECT_ALLOWLIST = ['https://ust1cmm.com']`, `resolveLegalRedirectUri()`, optional `VITE_LEGAL_*` overrides, `skipLegalClickwrapForAutomation()`. 3. **`ConnectedTermsGate`:** if no `address` (or automation skip) → `children`. Else wrap `TermsGate` with `network="TerraClassic"`, `appName="USTR CMM"`, sanitized `redirectUri`, styled `fallback` / `unsigned`. 4. **Mount** around `{children}` **inside** `Layout` `<main>` (or equivalent) so Header/Footer/WalletButton stay up. Do not gate the whole page chrome. 5. **Footer:** “Terms” → `https://terms.cl8y.com` or `GET …/terms/latest/content?property=ust1cmm.com` (new tab, `noopener noreferrer`). 6. **Ops (same issue, often a separate Legal Coolify change):** ```bash # from cl8y-ecosystem-legal checkout — script prompts for ADMIN_TOKEN (hidden) ./scripts/register-property.sh ust1cmm.com "USTR CMM" ./scripts/register-property.sh --list ``` Add `https://ust1cmm.com` to Legal API `CORS_ORIGINS` and portal `VITE_REDIRECT_URI_ALLOWLIST`; redeploy Legal API + web. 7. Style the unsigned card to existing Card / amber tokens. Button: “Accept Terms” → portal (SDK `handleAccept`). 8. Optional defense in depth: `useSwap` / `registerReferralCode` also no-op unless `signed_latest` (shell gate is primary). ```tsx // Sketch — disconnected users keep browse access function ConnectedTermsGate({ children }: { children: React.ReactNode }) { const { address } = useWallet(); if (!address) return <>{children}</>; return ( <TermsGate client={getLegalClickwrapClient()} property={getLegalProperty()} network="TerraClassic" account={address} redirectUri={resolveLegalRedirectUri() ?? undefined} appName="USTR CMM" > {children} </TermsGate> ); } ``` --- ## Acceptance criteria - [ ] `@plasticdigits/cl8y-clickwrap` installed via GitLab npm; no forked Terra verify / wallet submit in this repo. - [ ] Property **`ust1cmm.com`** used on every status + sign URL; registered on Legal admin. - [ ] Disconnected: Home / Treasury / Dashboard / Referral **browse** work; no “Checking terms…” wall. - [ ] Connected + `signed_latest: false` (or stale version): styled accept UI; **Swap Now** / register-code cannot fire; Header disconnect still works. - [ ] Connected + `signed_latest: true`: existing swap/referral UX unchanged. - [ ] Accept → full navigation to portal `/sign/terra-classic?property=ust1cmm.com` with allowlisted `redirect_uri` + `appName`. - [ ] Return + window focus → status re-poll → children shown without a second sign if already latest. - [ ] Legal API error / CORS fail after connect: fail-closed message, **not** the swap form. - [ ] Footer Terms link present. - [ ] Legal Coolify: `CORS_ORIGINS` + portal redirect allowlist include `https://ust1cmm.com`. - [ ] No `ADMIN_TOKEN`, no localhost redirect in prod bundles, no `signed_latest` durable localStorage cache. --- ## Test plan (all paths) **Disconnected / browse** - [ ] No wallet: Treasury, Home, Dashboard, Referral list render; **zero** Legal status calls (or only if you prefetch — prefer zero). - [ ] Footer Terms opens content/portal in a new tab. **Connect + unsigned** - [ ] Connect unsigned `terra1…` → one `GET /signatures/status?property=ust1cmm.com&network=TERRA_CLASSIC&account=…`. - [ ] Main shows accept panel (version label + Read full terms + Accept). - [ ] Swap / register CTAs not executable (hidden or disabled behind the gate). - [ ] Accept → `terms.cl8y.com/sign/terra-classic?property=ust1cmm.com&redirect_uri=https%3A%2F%2Fust1cmm.com%2F…`. - [ ] Disconnect from header while gated → browse restored. **Connect + signed** - [ ] `signed_latest: true` → children immediately; swap/register work as today. - [ ] Terms version bump on Legal → next focus/status shows re-accept (old version noted if SDK provides `signed_version`). **Return from portal** - [ ] Allowlisted `redirect_uri` returns to the same path (e.g. `/` or `/referral`). - [ ] Focus re-poll flips to signed without manual refresh. - [ ] Referral code in `/swap/:code` still in the URL / storage after round-trip. **Failure / env** - [ ] API 5xx / network error → fail-closed copy; no swap. - [ ] CORS missing `ust1cmm.com` → fail-closed (document ops fix). - [ ] `VITE_LEGAL_PROPERTY` staging override does not leak into the production default. - [ ] Prod build: `resolveLegalRedirectUri` rejects `http://localhost:5173`. - [ ] Localhost redirect allowed only in non-`PROD` (and only if portal `VITE_ALLOW_LOCALHOST_REDIRECT` is on for that env). **Ops** - [ ] `register-property.sh ust1cmm.com` listed; `GET /api/v1/terms/latest?property=ust1cmm.com` returns `sign_urls.terra_classic`. --- ## Test plan (attack, hack, abuse) | Vector | Risk | Test / guard | |--------|------|----------------| | **Raw `TermsGate` on `<App>`** | Anonymous users see only fallback; site looks down | Disconnected path renders children | | **Fail-open on API error** | Outage = unsigned users can swap | Error UI; `executeSwap` not reachable | | **`signed_latest` in localStorage** | User forges a flag | No durable cache; only API `signed_latest` | | **Wrong property** | Sign `cl8y.com`, use this site | Status + portal always `ust1cmm.com` | | **Wrong network (`EVM`)** | Status never matches `terra1` | Hard-code `TerraClassic` | | **Open redirect** | `redirect_uri=https://evil.example` | Client sanitize + portal allowlist; evil origin never auto-navigated | | **localhost redirect in prod** | Token/session bounce to attacker box | `allowLocalhost: !import.meta.env.PROD` | | **Iframe portal** | Clickjacking / weakened clickwrap | Full-page `window.location`; no iframe | | **Reimplement ADR-036 here** | Divergent verify, bad proofs | Grep: no `signArbitrary` / `submitWallet` in `frontend/src` except SDK | | **ADMIN_TOKEN in Vite env** | Admin Legal API from the browser | Grep env examples; public GETs only | | **CORS * or copy-paste origin** | Other sites read status (low) / confuse property | Ops: explicit `https://ust1cmm.com` only | | **Status poll hammer** | Many tabs × interval vs Legal rate limits | No `pollIntervalMs` unless ≥ focus-only; reuse singleton client | | **XSS → fake gate** | Injected “signed” UI | React text nodes; do not `innerHTML` terms | | **MITM Legal API** | Fake `signed_latest: true` | HTTPS defaults; do not allow user-supplied `apiBaseUrl` from query string | | **Address spoof in query** | Attacker checks someone else’s status (privacy) | Only the **connected** wallet address is sent | | **Playwright skip in prod** | `VITE_PLAYWRIGHT_E2E=true` baked into Coolify | Document: unset on ust1cmm.com builds; grep deploy env | | **Claim on-chain enforcement** | Users think contracts check T&Cs | Copy: Legal acceptance is off-chain; contracts unchanged | | **Referral deep-link loss** | `/swap/code` stripped by portal return | `redirect_uri` is current `href` including path | --- ## Verification criteria 1. On `https://ust1cmm.com` with a **new** wallet: connect → accept panel → portal Terra Classic sign → return → one successful status `signed_latest: true` → swap or register works. 2. Same wallet on a hard refresh: no portal bounce; children show. 3. DevTools: status URL has `property=ust1cmm.com` and `network=TERRA_CLASSIC`; **no** Legal admin routes; **no** BSC/EVM sign URLs. 4. Disconnected user can load `/treasury` with **no** Legal status request required. 5. Legal ops: property listed; CORS + redirect allowlist contain `https://ust1cmm.com` (Coolify Legal API + portal rebuild). 6. Grep clean: no `ADMIN_TOKEN` in `frontend/`; no custom ADR-036; property default is `ust1cmm.com`.
PlasticDigits commented 2026-08-15 09:49:56 +00:00 (Migrated from gitlab.com)

mentioned in commit 737f560578

mentioned in commit 737f5605783feb36f3e1acd6d8d5e5a57a689ad6
PlasticDigits commented 2026-08-15 09:49:58 +00:00 (Migrated from gitlab.com)

mentioned in merge request !32

mentioned in merge request !32
PlasticDigits commented 2026-08-15 09:50:06 +00:00 (Migrated from gitlab.com)

Implemented on !32.

Shipped: @plasticdigits/cl8y-clickwrap via GitLab npm, ConnectedTermsGate around Layout <main> (disconnected browse stays open), property ust1cmm.com / TerraClassic, sanitized redirect allowlist, Footer Terms, fail-closed swap/register. Playbook: skills/frontend-legal-clickwrap/SKILL.md.

Not done here (Legal platform ops): register-property.sh ust1cmm.com, API CORS_ORIGINS, portal VITE_REDIRECT_URI_ALLOWLIST. Keep VITE_PLAYWRIGHT_E2E unset on ust1cmm.com builds.

Implemented on !32. Shipped: `@plasticdigits/cl8y-clickwrap` via GitLab npm, `ConnectedTermsGate` around Layout `<main>` (disconnected browse stays open), property `ust1cmm.com` / `TerraClassic`, sanitized redirect allowlist, Footer Terms, fail-closed swap/register. Playbook: `skills/frontend-legal-clickwrap/SKILL.md`. **Not done here (Legal platform ops):** `register-property.sh ust1cmm.com`, API `CORS_ORIGINS`, portal `VITE_REDIRECT_URI_ALLOWLIST`. Keep `VITE_PLAYWRIGHT_E2E` unset on ust1cmm.com builds.
PlasticDigits commented 2026-08-15 10:23:19 +00:00 (Migrated from gitlab.com)

mentioned in commit 09a54333aa

mentioned in commit 09a54333aaaaf58423d04efe6a209b08e54d5cd7
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-15 10:23:19 +00:00
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/ustr-cmm#12
No description provided.