design: TermsGate coverage parity for cl8y.com and bridge.cl8y.com #34

Open
opened 2026-09-05 16:11:21 +00:00 by PlasticDigits · 2 comments

Summary

Record TermsGate coverage parity for the two first-party hosts that are not the DEX, then implement the missing Legal-side work.

Decisions (do not re-litigate in a follow-up unless product changes them):

Host Decision Why
https://dex.cl8y.com Gate (already shipped) Wallet-connected transactional SPA. Reference: cl8y-dex-terraclassic#517 (ConnectedTermsGate, property dex.cl8y.com).
https://bridge.cl8y.com Gate like DEX Wallet-connected mutative transfer / execute UI. Frontend already merged on cl8y-bridge-monorepo#134. This issue completes Legal property + CORS + portal redirect allowlist so production status checks can succeed. Do not re-implement BridgeTermsGate.
https://cl8y.com Explicit exception — do not mount TermsGate Marketing SPA. No Connect Wallet, no swaps/deposits/votes. @plasticdigits/cl8y-clickwrap TermsGate requires a network + account; gating this host like DEX would force a wallet-sign flow on an informational site (or a fake account). Document the exception; optional footer link to the hosted T&Cs reader, not a signature.

Bundle (do not split):

  1. Integrator coverage matrix in this repo (skill + README examples).
  2. Legal ops for bridge.cl8y.com (same playbook as #8 / #12).
  3. Marketing exception in code/CL8Y-web (invariants + optional Terms href). Separate worktree; do not patch CL8Y-web from a Legal checkout.

Not a duplicate of #134 (frontend wiring is done there; that issue stays open until production verify). Not a duplicate of DEX #517 (closed). Not a duplicate of CL8Y-web#12 (reserved /privacy / /cookies / /opt-out paths — T&Cs are not a Privacy Notice).


Current codebase

Acceptance is per property (hostname). One signature row per (property, terms version, network, account). Signing on cl8y.com does not satisfy bridge.cl8y.com or dex.cl8y.com.

Piece Path / behavior
SDK packages/cl8y-clickwrap — createClient, TermsGate, useSignatureStatus, buildSignUrl. TermsGate with empty account renders fallback, not children. Networks: EVM / TerraClassic / Solana / Telegram only. There is no anonymous/web-session network.
Portal web/ at https://terms.cl8y.com. Redirects only to origins on VITE_REDIRECT_URI_ALLOWLIST.
API api/ at https://api.terms.cl8y.com. Browser status calls need the dapp origin on CORS_ORIGINS.
Register scripts/register-property.sh → POST /admin/properties. Public GET /api/v1/terms/latest?property= can upsert a website row; still register display names explicitly for production.
README examples Still treat cl8y.com as the canonical website property (GET …?property=cl8y.com, portal allowlist example https://cl8y.com,https://www.cl8y.com). That reads as if the marketing site were a gated integrator. Live gated dapps are dex.cl8y.com (and, once ops land, bridge.cl8y.com). Other already-registered properties (ust1cmm.com, vote.cl8y.com) are out of scope except as playbook references.

DEX (code/cl8y-dex-terraclassic) — reference only

  • frontend-dapp/src/components/legal/ConnectedTermsGate.tsx wraps connected Terra Classic address with TermsGate (property default dex.cl8y.com, appName="CL8Y DEX").
  • frontend-dapp/src/utils/legalClickwrap.ts — client singleton, production HTTPS bases, sanitizeRedirectUri, Playwright skip only via VITE_PLAYWRIGHT_E2E.
  • Dependency: @plasticdigits/cl8y-clickwrap@^0.1.1.
  • Disconnected browse stays open; first-visit RiskAcknowledgementModal is not clickwrap.
  • @plasticdigits/cl8y-clickwrap@0.1.1 in packages/frontend/package.json.
  • Property hard-coded bridge.cl8y.com in packages/frontend/src/utils/clickwrap.ts (BRIDGE_CLICKWRAP_PROPERTY). Must not be derived from window.location.hostname.
  • BridgeTermsGate wraps mutative transfer/status controls; header Connect stays outside the gate (packages/frontend/src/components/transfer/BridgeTermsGate.tsx).
  • useBridgeClickwrapGate — allowsMutative only after successful signed_latest (fail closed while loading / on error).
  • Tests: packages/frontend/e2e/clickwrap.spec.ts, Vitest around clickwrap.ts / BridgeTermsGate.
  • Invariant INV-FE-CLICKWRAP-1 in docs/FRONTEND_BRIDGE_INVARIANTS.md + skills/agent-frontend-clickwrap.md.
  • Remaining on #134: register property, API CORS, portal allowlist, Solana portal readiness (fail-closed — do not skip Solana legal), manual portal round-trip. Do not close #134 from this issue.

Marketing (code/CL8Y-web) — no TermsGate

  • Vite + React 19 SPA. Routes in src/app/index.tsx: /, blog, legacy section redirects, whitepaper PDF redirect. No /sign, no clickwrap, no Connect Wallet chrome.
  • Canonical product hrefs: src/content/invariants.ts CANONICAL_PRODUCT_URLS = Bridge + DEX only. Footer (src/components/chrome/SiteFooter.tsx) has disclaimer + law-enforcement copy; no link to https://terms.cl8y.com.
  • AppProviders still mounts WagmiProvider (reconnectOnMount) with optional WalletConnect when VITE_WALLETCONNECT_PROJECT_ID is set (src/providers/AppProviders.tsx). That is leftover read-path infra, not a user-facing wallet gate. Do not reuse it to fake a TermsGate account.
  • No @plasticdigits/cl8y-clickwrap dependency.

Why this is needed

  1. Uneven legal evidence. DEX users cannot transact until signed_latest for dex.cl8y.com. Bridge code will fail-closed in production until Legal allows the origin. Marketing visitors can read product CTAs with no wallet-bound T&Cs — that is acceptable only if it is an explicit, tested exception, not an accident.
  2. Property confusion in Legal docs. README and web/.env.example still hold up cl8y.com as the default website property. Operators and agents will register or CORS-allow the apex “to match the docs” and then wonder why the marketing SPA never calls /signatures/status.
  3. SDK cannot gate a wallet-less site. TermsGate is account-scoped. Mounting it on cl8y.com without a connected address shows the checking fallback forever (or blocks the whole page). Inventing a synthetic account, a localStorage “I agree”, or a query signed=1 is not Legal acceptance.
  4. One matrix. Splitting “fix README” vs “register bridge” vs “marketing footer” produces drift (bridge gated in code, ungated in prod, apex described as a property with no integrator).

This is coverage policy + Legal env/allowlists + marketing documentation (and an optional Terms link). No new signing crypto. No bridge/DEX clickwrap rewrite.


Constraints and guardrails

  1. Do not re-implement portal sign pages, ADR-036 verify, or a parallel checkbox that claims to satisfy Legal.
  2. Do not treat T&Cs as a Privacy Notice. CL8Y-web#12 owns /privacy / /cookies / /opt-out. A footer “Terms” href must go to https://terms.cl8y.com (or /api/v1/terms/latest/content?property=… only as a raw text link). Never label it Privacy / cookies / opt-out.
  3. Bridge property is bridge.cl8y.com exactly. Do not reuse cl8y.com, dex.cl8y.com, www.bridge.cl8y.com (unless that host is actually served — it is not the SPA origin).
  4. Marketing exception is fail-open for browse, fail-closed for fake acceptance. Visitors browse. There is no signed_latest for the marketing host. Do not write localStorage / cookies / query flags that later dapps could trust.
  5. No wallet connect for clickwrap on cl8y.com. Do not add a Connect control, do not pass wagmi useAccount into TermsGate, do not add the clickwrap package to CL8Y-web.
  6. CORS / allowlist: exact https://bridge.cl8y.com (no trailing slash, no *). #7 wildcards do not replace this. Do not add https://cl8y.com to production CORS because of this issue (a simple <a href> to the portal needs no CORS). If cl8y.com is already on an allowlist from older examples, do not treat that as “marketing is gated.”
  7. Redirect safety: portal remains enforcer. Bridge already passes same-origin window.location.href only. Marketing Terms link is a compile-time HTTPS constant (URL / existing ExternalLink), never from query / hash / Referer.
  8. Secrets: no ADMIN_TOKEN in any frontend. register-property.sh prompts hidden; do not paste tokens into the issue, comments, or env examples beyond the existing placeholder pattern.
  9. Solana: bridge must keep fail-closed if /sign/solana cannot produce a verifiable signature. Do not skip Solana legal while gating EVM/Terra.
  10. Worktree isolation. Legal PRs/docs here. CL8Y-web sibling PR for invariants + footer. Bridge frontend changes are out of scope unless ops verify finds a property-string bug (then fix on #134, not here).
  11. Do not change terms text / version oracle (#6), clickjacking headers, SPA legal-guess 404s (#12 on CL8Y-web), or storage-consent / WalletConnect pulse (cl8y-bridge-monorepo#165).
  12. Do not publish host/VM/inventory details in the MR. Describe API/portal env keys only.

Relevant files

Path Why
README.md Replace cl8y.com as the default gated-website example with dex.cl8y.com; add the coverage table
skills/README.md Link a new integrator-coverage skill
skills/integrator-coverage/SKILL.md New — host matrix, exception rules, ops playbook pointer
scripts/register-property.sh Ops path for bridge.cl8y.com
.env.example Comment: dapp origins on CORS_ORIGINS (example https://dex.cl8y.com, https://bridge.cl8y.com) — not the marketing apex as a required integrator
web/.env.example Redirect allowlist example should list gated dapps, not imply the marketing SPA signs
packages/cl8y-clickwrap/README.md One sentence: TermsGate is for wallet-connected integrators; marketing hosts need an explicit exception
skills/security-ops/SKILL.md Cross-link: allowlist additions for new gated properties only

Sibling code/CL8Y-web (separate PR)

Path Why
src/content/invariants.ts Constant for Terms URL + coverage exception comment; keep CANONICAL_PRODUCT_URLS as Bridge/DEX
src/content/invariants.test.ts Assert Terms href is exact HTTPS, no @ / redirectors; assert clickwrap package is not a dependency
src/data/products.ts or src/lib/siteUrls.ts Export https://terms.cl8y.com next to other first-party URLs
src/components/chrome/SiteFooter.tsx + src/data/copy.ts Optional “Terms” link; do not call it Privacy
skills/cl8y-site-positioning/invariants.md Document: no TermsGate on this host
package.json Must remain without @plasticdigits/cl8y-clickwrap

Sibling code/cl8y-bridge-monorepo (read / verify only)

Path Why
packages/frontend/src/utils/clickwrap.ts Property pin bridge.cl8y.com
packages/frontend/src/components/transfer/BridgeTermsGate.tsx Mutative gate
docs/FRONTEND_BRIDGE_INVARIANTS.md INV-FE-CLICKWRAP-1

Reference code/cl8y-dex-terraclassic

Path Why
frontend-dapp/src/components/legal/ConnectedTermsGate.tsx Shell-vs-CTA pattern already chosen for DEX

1. Coverage matrix (this repo)

Add skills/integrator-coverage/SKILL.md with a table agents must copy:

Property / origin Integrator repo TermsGate? Networks Tracking
dex.cl8y.com code/cl8y-dex-terraclassic Yes — connected Terra Classic shell TerraClassic #517 closed
bridge.cl8y.com code/cl8y-bridge-monorepo Yes — mutative CTAs, not app chrome EVM, Terra Classic, Solana #134 frontend; this issue Legal ops
cl8y.com code/CL8Y-web No — documented exception n/a this issue
ust1cmm.com / vote.cl8y.com other products Yes (already) see #8 / #12 out of scope

Rules in the skill:

  • Wallet-connected mutative UI → SDK TermsGate + dedicated property + CORS + allowlist.
  • Informational / marketing host → exception in this table; link to portal for reading T&Cs; no status polling.
  • Never share properties across hosts.

Update README.md public API examples to property=dex.cl8y.com. Keep a short note that cl8y.com is the marketing origin and is not a required clickwrap property.

Same as #8 / #12:

./scripts/register-property.sh bridge.cl8y.com "CL8Y Bridge"
./scripts/register-property.sh --list

Append exact origin (keep existing entries):

  • API CORS_ORIGINS=…,https://bridge.cl8y.com
  • Portal VITE_REDIRECT_URI_ALLOWLIST=…,https://bridge.cl8y.com and rebuild the static portal (Vite inlines the allowlist).

Do not enable VITE_ALLOW_LOCALHOST_REDIRECT on the production portal.

Verify (no secrets in output):

curl -sS 'https://api.terms.cl8y.com/api/v1/terms/latest?property=bridge.cl8y.com' \
  | jq '{property, version_label, evm: .sign_urls.evm, terra: .sign_urls.terra_classic, solana: .sign_urls.solana}'

curl -sSI -X OPTIONS 'https://api.terms.cl8y.com/api/v1/signatures/status' \
  -H 'Origin: https://bridge.cl8y.com' \
  -H 'Access-Control-Request-Method: GET' \
  | grep -i 'access-control-allow-origin'
# expect: Access-Control-Allow-Origin: https://bridge.cl8y.com

Negative check: Origin: https://cl8y.com must not be required for this ticket. Optional: confirm a marketing origin preflight is irrelevant because CL8Y-web will not call the status API.

3. Marketing exception (CL8Y-web sibling)

  • Add LEGAL_TERMS_URL = "https://terms.cl8y.com" (exact, no path required; portal home shows T&Cs).
  • Footer link label Terms (or Terms & Conditions), ExternalLink, rel already used by the footer helper.
  • Invariants test: URL constant; package.json / lockfile has no cl8y-clickwrap; no TermsGate import under src/.
  • Skill note: do not add TermsGate without changing this exception in Legal’s matrix first.

If product later reverses the exception, that is a new issue (would require a wallet UX on the marketing site or an SDK change for anonymous acceptance — both out of scope here).


Acceptance criteria

  • AC1. This repo documents the three-row matrix (DEX gated, Bridge gated, cl8y.com explicit exception) in a skill + README. Examples no longer imply the marketing SPA is the default clickwrap integrator.
  • AC2. Property bridge.cl8y.com exists on the Legal API (GET /api/v1/terms/latest?property=bridge.cl8y.com returns that property + sign URLs).
  • AC3. Browser CORS preflight from Origin: https://bridge.cl8y.com to /api/v1/signatures/status returns Access-Control-Allow-Origin: https://bridge.cl8y.com.
  • AC4. Portal allowlist includes https://bridge.cl8y.com so post-sign navigation can return to the bridge (rebuild done). Production portal still rejects unlisted redirect_uri.
  • AC5. CL8Y-web sibling: no TermsGate / no clickwrap dependency; exception documented in invariants/skill; optional Terms footer link is the constant https://terms.cl8y.com and is not named Privacy.
  • AC6. CL8Y-web does not call /api/v1/signatures/status (grep / e2e: no Legal API from the marketing origin).
  • AC7. Bridge frontend property string remains bridge.cl8y.com (no change unless a bug). #134 stays the frontend tracker.
  • AC8. No ADMIN_TOKEN added to any SPA. No dangerouslySetInnerHTML for terms HTML. No open redirect from the marketing Terms href.
  • AC9. Solana on the bridge remains fail-closed; this issue does not add a “skip Solana legal” flag.

Test plan (functional paths)

# Path Expect
T1 GET /api/v1/terms/latest?property=bridge.cl8y.com 200; property is bridge.cl8y.com; evm/terra/solana sign_urls present
T2 GET /api/v1/terms/latest?property=dex.cl8y.com Unchanged (DEX still registered)
T3 OPTIONS status from Origin: https://bridge.cl8y.com ACAO echo of that origin
T4 OPTIONS status from Origin: https://evil.example No ACAO for attacker origin
T5 Portal sign URL with redirect_uri=https://bridge.cl8y.com/… Allowed after rebuild
T6 Portal sign URL with redirect_uri=https://phish.example/ No auto-navigate (existing portal tests in web/e2e/redirect.spec.ts stay green)
T7 README / skill table DEX yes, Bridge yes, cl8y.com exception
T8 CL8Y-web / Homepage unchanged; Terms link (if added) opens https://terms.cl8y.com in a new tab via ExternalLink
T9 CL8Y-web unit tests Terms URL constant; no clickwrap dep
T10 CL8Y-web Playwright (--workers=5) Homepage CTAs still Bridge/DEX; reserved legal-guess paths remain #12’s job (do not 200 a fake Privacy page from this ticket)
T11 Bridge e2e clickwrap.spec.ts Still green with mocked Legal; production round-trip is #134 verify after AC2–AC4
T12 Marketing GET https://cl8y.com/ (or preview) No redirect to /sign/evm; no TermsGate overlay
T13 GET …/terms/latest?property=cl8y.com May upsert/exist historically; must not be treated as “marketing is gated” in the matrix

Legal portal Playwright: existing web/e2e suite, 5 workers as already configured. No new wallet-sign e2e required in CL8Y-web.


Test plan (attack, hack, and abuse)

# Vector Expect
A1 Use a cl8y.com signature as evidence on the bridge API keys by property; bridge client sends bridge.cl8y.com only (clickwrap.ts unit tests already pin this)
A2 Derive bridge property from window.location.hostname / Host header spoof Forbidden; constant BRIDGE_CLICKWRAP_PROPERTY
A3 Mount TermsGate on CL8Y-web with a hardcoded dummy account Forbidden by exception + grep AC6
A4 localStorage / cookie signed_latest=true on cl8y.com Must not exist; DEX/bridge must not read marketing storage
A5 Query ?signed=1 / ?property=dex.cl8y.com on marketing Ignored; no status client
A6 Footer Terms href from ?next= / javascript: / //evil Impossible; compile-time https://terms.cl8y.com
A7 Open redirect via portal redirect_uri crafted by marketing Marketing must not call buildSignUrl
A8 CORS * to “make bridge work” Reject; exact origin only
A9 Add https://cl8y.com to CORS so a future silent status poll can run Out of policy; exception says no status poll from marketing
A10 Skip Solana legal on bridge because portal envelope gap exists Fail closed; document in #134, do not add skip env here
A11 XSS via terms HTML in the marketing footer Text link only; no innerHTML of API content
A12 Clickjack the portal from cl8y.com Do not iframe terms.cl8y.com; ExternalLink / top navigation. Marketing already intends frame-ancestors 'none' (CL8Y-web host-headers skill)
A13 Phishing clone of cl8y.com that points SDK at an attacker API N/A if no SDK; if someone adds it later, pin SDK defaults like DEX/bridge
A14 Register www.cl8y.com and claim parity Apex marketing exception applies to the served marketing origin; do not invent extra gated properties without an integrator
A15 Wagmi reconnectOnMount supplies an 0x and someone wires TermsGate “because the provider exists” Explicitly banned; leftover wagmi is not a clickwrap account source

Verification criteria

  1. Docs: skills/integrator-coverage/SKILL.md exists; skills/README.md links it; README API examples use a gated dapp property (dex.cl8y.com), not “marketing is gated.”
  2. API: curls in T1 and T3 pass from a public network (no admin token in logs).
  3. Portal: allowlist rebuild confirmed by a bridge redirect_uri success path or by reading the built allowlist constant in the portal bundle (string https://bridge.cl8y.com present; attacker origin absent).
  4. CL8Y-web sibling: yarn test, yarn typecheck, yarn test:e2e --workers=5 green; no clickwrap in package.json; footer Terms (if present) is the constant.
  5. Bridge: no required code change. After AC2–AC4, #134 manual unsigned → portal → return → deposit CTA can be verified on that issue.
  6. Grep: TermsGate / cl8y-clickwrap absent under code/CL8Y-web/src.

Out of scope

  • Re-wiring BridgeTermsGate / DEX ConnectedTermsGate.
  • Authoring a Privacy Notice, cookie banner, or CMP (CL8Y-web#12 and bridge#165).
  • Publishing @plasticdigits/cl8y-clickwrap to npmjs (#14) — integrators already consume 0.1.1.
  • yieldomega.com / other hosts not in the table (add rows in a new issue if they grow a wallet UI).
  • Telegram clickwrap on web properties.
  • Changing Legal message format / Content-SHA256 oracle.
  • Removing unused wagmi from CL8Y-web (separate cleanup).

References

## Summary Record **TermsGate coverage parity** for the two first-party hosts that are not the DEX, then implement the missing Legal-side work. **Decisions (do not re-litigate in a follow-up unless product changes them):** | Host | Decision | Why | |------|----------|-----| | `https://dex.cl8y.com` | **Gate** (already shipped) | Wallet-connected transactional SPA. Reference: [cl8y-dex-terraclassic#517](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/517) (`ConnectedTermsGate`, property `dex.cl8y.com`). | | `https://bridge.cl8y.com` | **Gate like DEX** | Wallet-connected mutative transfer / execute UI. Frontend already merged on [cl8y-bridge-monorepo#134](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/134). This issue completes **Legal property + CORS + portal redirect allowlist** so production status checks can succeed. Do **not** re-implement `BridgeTermsGate`. | | `https://cl8y.com` | **Explicit exception — do not mount TermsGate** | Marketing SPA. No Connect Wallet, no swaps/deposits/votes. `@plasticdigits/cl8y-clickwrap` `TermsGate` requires a `network` + `account`; gating this host like DEX would force a wallet-sign flow on an informational site (or a fake account). Document the exception; optional footer link to the hosted T&Cs **reader**, not a signature. | Bundle (do not split): 1. **Integrator coverage matrix** in this repo (skill + README examples). 2. **Legal ops for `bridge.cl8y.com`** (same playbook as [#8](https://git.cl8y.com/code/cl8y-ecosystem-legal/issues/8) / [#12](https://git.cl8y.com/code/cl8y-ecosystem-legal/issues/12)). 3. **Marketing exception** in `code/CL8Y-web` (invariants + optional Terms href). Separate worktree; do not patch CL8Y-web from a Legal checkout. Not a duplicate of #134 (frontend wiring is done there; that issue stays open until production verify). Not a duplicate of DEX #517 (closed). Not a duplicate of [CL8Y-web#12](https://git.cl8y.com/code/CL8Y-web/issues/12) (reserved `/privacy` / `/cookies` / `/opt-out` paths — T&Cs are not a Privacy Notice). --- ## Current codebase ### Legal platform (`code/cl8y-ecosystem-legal`) — this worktree Acceptance is **per property** (hostname). One signature row per `(property, terms version, network, account)`. Signing on `cl8y.com` does **not** satisfy `bridge.cl8y.com` or `dex.cl8y.com`. | Piece | Path / behavior | |-------|-----------------| | SDK | `packages/cl8y-clickwrap` — `createClient`, `TermsGate`, `useSignatureStatus`, `buildSignUrl`. `TermsGate` with empty `account` renders `fallback`, not children. Networks: `EVM` / `TerraClassic` / `Solana` / `Telegram` only. There is **no** anonymous/web-session network. | | Portal | `web/` at `https://terms.cl8y.com`. Redirects only to origins on `VITE_REDIRECT_URI_ALLOWLIST`. | | API | `api/` at `https://api.terms.cl8y.com`. Browser status calls need the dapp origin on `CORS_ORIGINS`. | | Register | `scripts/register-property.sh` → `POST /admin/properties`. Public `GET /api/v1/terms/latest?property=` can upsert a website row; still register display names explicitly for production. | | README examples | Still treat **`cl8y.com`** as the canonical website property (`GET …?property=cl8y.com`, portal allowlist example `https://cl8y.com,https://www.cl8y.com`). That reads as if the marketing site were a gated integrator. Live gated dapps are `dex.cl8y.com` (and, once ops land, `bridge.cl8y.com`). Other already-registered properties (`ust1cmm.com`, `vote.cl8y.com`) are out of scope except as playbook references. | ### DEX (`code/cl8y-dex-terraclassic`) — reference only - `frontend-dapp/src/components/legal/ConnectedTermsGate.tsx` wraps connected Terra Classic `address` with `TermsGate` (`property` default `dex.cl8y.com`, `appName="CL8Y DEX"`). - `frontend-dapp/src/utils/legalClickwrap.ts` — client singleton, production HTTPS bases, `sanitizeRedirectUri`, Playwright skip only via `VITE_PLAYWRIGHT_E2E`. - Dependency: `@plasticdigits/cl8y-clickwrap@^0.1.1`. - Disconnected browse stays open; first-visit `RiskAcknowledgementModal` is **not** clickwrap. ### Bridge (`code/cl8y-bridge-monorepo`) — frontend done, Legal ops not - `@plasticdigits/cl8y-clickwrap@0.1.1` in `packages/frontend/package.json`. - Property **hard-coded** `bridge.cl8y.com` in `packages/frontend/src/utils/clickwrap.ts` (`BRIDGE_CLICKWRAP_PROPERTY`). Must **not** be derived from `window.location.hostname`. - `BridgeTermsGate` wraps **mutative** transfer/status controls; header Connect stays outside the gate (`packages/frontend/src/components/transfer/BridgeTermsGate.tsx`). - `useBridgeClickwrapGate` — `allowsMutative` only after successful `signed_latest` (fail closed while loading / on error). - Tests: `packages/frontend/e2e/clickwrap.spec.ts`, Vitest around `clickwrap.ts` / `BridgeTermsGate`. - Invariant **INV-FE-CLICKWRAP-1** in `docs/FRONTEND_BRIDGE_INVARIANTS.md` + `skills/agent-frontend-clickwrap.md`. - Remaining on #134: register property, API CORS, portal allowlist, Solana portal readiness (fail-closed — do not skip Solana legal), manual portal round-trip. **Do not close #134 from this issue.** ### Marketing (`code/CL8Y-web`) — no TermsGate - Vite + React 19 SPA. Routes in `src/app/index.tsx`: `/`, blog, legacy section redirects, whitepaper PDF redirect. **No** `/sign`, **no** clickwrap, **no** Connect Wallet chrome. - Canonical product hrefs: `src/content/invariants.ts` `CANONICAL_PRODUCT_URLS` = Bridge + DEX only. Footer (`src/components/chrome/SiteFooter.tsx`) has disclaimer + law-enforcement copy; **no** link to `https://terms.cl8y.com`. - `AppProviders` still mounts `WagmiProvider` (`reconnectOnMount`) with optional WalletConnect when `VITE_WALLETCONNECT_PROJECT_ID` is set (`src/providers/AppProviders.tsx`). That is leftover read-path infra, **not** a user-facing wallet gate. Do **not** reuse it to fake a TermsGate account. - No `@plasticdigits/cl8y-clickwrap` dependency. --- ## Why this is needed 1. **Uneven legal evidence.** DEX users cannot transact until `signed_latest` for `dex.cl8y.com`. Bridge code will fail-closed in production until Legal allows the origin. Marketing visitors can read product CTAs with no wallet-bound T&Cs — that is acceptable **only if** it is an explicit, tested exception, not an accident. 2. **Property confusion in Legal docs.** README and `web/.env.example` still hold up `cl8y.com` as the default website property. Operators and agents will register or CORS-allow the apex “to match the docs” and then wonder why the marketing SPA never calls `/signatures/status`. 3. **SDK cannot gate a wallet-less site.** `TermsGate` is account-scoped. Mounting it on `cl8y.com` without a connected address shows the checking fallback forever (or blocks the whole page). Inventing a synthetic account, a localStorage “I agree”, or a query `signed=1` is not Legal acceptance. 4. **One matrix.** Splitting “fix README” vs “register bridge” vs “marketing footer” produces drift (bridge gated in code, ungated in prod, apex described as a property with no integrator). This is **coverage policy + Legal env/allowlists + marketing documentation (and an optional Terms link)**. No new signing crypto. No bridge/DEX clickwrap rewrite. --- ## Constraints and guardrails 1. **Do not re-implement** portal sign pages, ADR-036 verify, or a parallel checkbox that claims to satisfy Legal. 2. **Do not treat T&Cs as a Privacy Notice.** [CL8Y-web#12](https://git.cl8y.com/code/CL8Y-web/issues/12) owns `/privacy` / `/cookies` / `/opt-out`. A footer “Terms” href must go to `https://terms.cl8y.com` (or `/api/v1/terms/latest/content?property=…` only as a raw text link). Never label it Privacy / cookies / opt-out. 3. **Bridge property is `bridge.cl8y.com` exactly.** Do not reuse `cl8y.com`, `dex.cl8y.com`, `www.bridge.cl8y.com` (unless that host is actually served — it is not the SPA origin). 4. **Marketing exception is fail-open for browse, fail-closed for fake acceptance.** Visitors browse. There is no `signed_latest` for the marketing host. Do not write `localStorage` / cookies / query flags that later dapps could trust. 5. **No wallet connect for clickwrap on cl8y.com.** Do not add a Connect control, do not pass wagmi `useAccount` into `TermsGate`, do not add the clickwrap package to CL8Y-web. 6. **CORS / allowlist:** exact `https://bridge.cl8y.com` (no trailing slash, no `*`). [#7](https://git.cl8y.com/code/cl8y-ecosystem-legal/issues/7) wildcards do **not** replace this. Do **not** add `https://cl8y.com` to production CORS **because of this issue** (a simple `<a href>` to the portal needs no CORS). If `cl8y.com` is already on an allowlist from older examples, do not treat that as “marketing is gated.” 7. **Redirect safety:** portal remains enforcer. Bridge already passes same-origin `window.location.href` only. Marketing Terms link is a compile-time HTTPS constant (`URL` / existing `ExternalLink`), never from query / hash / `Referer`. 8. **Secrets:** no `ADMIN_TOKEN` in any frontend. `register-property.sh` prompts hidden; do not paste tokens into the issue, comments, or env examples beyond the existing placeholder pattern. 9. **Solana:** bridge must keep fail-closed if `/sign/solana` cannot produce a verifiable signature. Do not skip Solana legal while gating EVM/Terra. 10. **Worktree isolation.** Legal PRs/docs here. CL8Y-web sibling PR for invariants + footer. Bridge frontend changes are **out of scope** unless ops verify finds a property-string bug (then fix on #134, not here). 11. **Do not change** terms text / version oracle ([#6](https://git.cl8y.com/code/cl8y-ecosystem-legal/issues/6)), clickjacking headers, SPA legal-guess 404s (#12 on CL8Y-web), or storage-consent / WalletConnect pulse ([cl8y-bridge-monorepo#165](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/165)). 12. **Do not publish** host/VM/inventory details in the MR. Describe API/portal **env keys** only. --- ## Relevant files ### This repo (`code/cl8y-ecosystem-legal`) | Path | Why | |------|-----| | `README.md` | Replace `cl8y.com` as the default gated-website example with `dex.cl8y.com`; add the coverage table | | `skills/README.md` | Link a new integrator-coverage skill | | `skills/integrator-coverage/SKILL.md` | **New** — host matrix, exception rules, ops playbook pointer | | `scripts/register-property.sh` | Ops path for `bridge.cl8y.com` | | `.env.example` | Comment: dapp origins on `CORS_ORIGINS` (example `https://dex.cl8y.com`, `https://bridge.cl8y.com`) — not the marketing apex as a required integrator | | `web/.env.example` | Redirect allowlist example should list gated dapps, not imply the marketing SPA signs | | `packages/cl8y-clickwrap/README.md` | One sentence: TermsGate is for wallet-connected integrators; marketing hosts need an explicit exception | | `skills/security-ops/SKILL.md` | Cross-link: allowlist additions for new **gated** properties only | ### Sibling `code/CL8Y-web` (separate PR) | Path | Why | |------|-----| | `src/content/invariants.ts` | Constant for Terms URL + coverage exception comment; keep `CANONICAL_PRODUCT_URLS` as Bridge/DEX | | `src/content/invariants.test.ts` | Assert Terms href is exact HTTPS, no `@` / redirectors; assert clickwrap package is not a dependency | | `src/data/products.ts` or `src/lib/siteUrls.ts` | Export `https://terms.cl8y.com` next to other first-party URLs | | `src/components/chrome/SiteFooter.tsx` + `src/data/copy.ts` | Optional “Terms” link; do not call it Privacy | | `skills/cl8y-site-positioning/invariants.md` | Document: no TermsGate on this host | | `package.json` | Must remain without `@plasticdigits/cl8y-clickwrap` | ### Sibling `code/cl8y-bridge-monorepo` (read / verify only) | Path | Why | |------|-----| | `packages/frontend/src/utils/clickwrap.ts` | Property pin `bridge.cl8y.com` | | `packages/frontend/src/components/transfer/BridgeTermsGate.tsx` | Mutative gate | | `docs/FRONTEND_BRIDGE_INVARIANTS.md` | INV-FE-CLICKWRAP-1 | ### Reference `code/cl8y-dex-terraclassic` | Path | Why | |------|-----| | `frontend-dapp/src/components/legal/ConnectedTermsGate.tsx` | Shell-vs-CTA pattern already chosen for DEX | --- ## Recommended direction ### 1. Coverage matrix (this repo) Add `skills/integrator-coverage/SKILL.md` with a table agents must copy: | Property / origin | Integrator repo | TermsGate? | Networks | Tracking | |-------------------|-----------------|------------|----------|----------| | `dex.cl8y.com` | `code/cl8y-dex-terraclassic` | Yes — connected Terra Classic shell | `TerraClassic` | #517 closed | | `bridge.cl8y.com` | `code/cl8y-bridge-monorepo` | Yes — mutative CTAs, not app chrome | EVM, Terra Classic, Solana | #134 frontend; **this issue** Legal ops | | `cl8y.com` | `code/CL8Y-web` | **No** — documented exception | n/a | this issue | | `ust1cmm.com` / `vote.cl8y.com` | other products | Yes (already) | see #8 / #12 | out of scope | Rules in the skill: - Wallet-connected mutative UI → SDK TermsGate + dedicated property + CORS + allowlist. - Informational / marketing host → exception in this table; link to portal for **reading** T&Cs; no status polling. - Never share properties across hosts. Update `README.md` public API examples to `property=dex.cl8y.com`. Keep a short note that `cl8y.com` is the marketing origin and is **not** a required clickwrap property. ### 2. Legal ops for the bridge (this repo / API+portal env) Same as #8 / #12: ```bash ./scripts/register-property.sh bridge.cl8y.com "CL8Y Bridge" ./scripts/register-property.sh --list ``` Append exact origin (keep existing entries): - API `CORS_ORIGINS=…,https://bridge.cl8y.com` - Portal `VITE_REDIRECT_URI_ALLOWLIST=…,https://bridge.cl8y.com` and **rebuild** the static portal (Vite inlines the allowlist). Do not enable `VITE_ALLOW_LOCALHOST_REDIRECT` on the production portal. Verify (no secrets in output): ```bash curl -sS 'https://api.terms.cl8y.com/api/v1/terms/latest?property=bridge.cl8y.com' \ | jq '{property, version_label, evm: .sign_urls.evm, terra: .sign_urls.terra_classic, solana: .sign_urls.solana}' curl -sSI -X OPTIONS 'https://api.terms.cl8y.com/api/v1/signatures/status' \ -H 'Origin: https://bridge.cl8y.com' \ -H 'Access-Control-Request-Method: GET' \ | grep -i 'access-control-allow-origin' # expect: Access-Control-Allow-Origin: https://bridge.cl8y.com ``` Negative check: `Origin: https://cl8y.com` must **not** be required for this ticket. Optional: confirm a marketing origin preflight is irrelevant because CL8Y-web will not call the status API. ### 3. Marketing exception (CL8Y-web sibling) - Add `LEGAL_TERMS_URL = "https://terms.cl8y.com"` (exact, no path required; portal home shows T&Cs). - Footer link label **Terms** (or **Terms & Conditions**), `ExternalLink`, `rel` already used by the footer helper. - Invariants test: URL constant; `package.json` / lockfile has no `cl8y-clickwrap`; no `TermsGate` import under `src/`. - Skill note: do not add TermsGate without changing this exception in Legal’s matrix first. If product later reverses the exception, that is a **new** issue (would require a wallet UX on the marketing site or an SDK change for anonymous acceptance — both out of scope here). --- ## Acceptance criteria - [ ] **AC1.** This repo documents the three-row matrix (DEX gated, Bridge gated, `cl8y.com` explicit exception) in a skill + README. Examples no longer imply the marketing SPA is the default clickwrap integrator. - [ ] **AC2.** Property `bridge.cl8y.com` exists on the Legal API (`GET /api/v1/terms/latest?property=bridge.cl8y.com` returns that property + sign URLs). - [ ] **AC3.** Browser CORS preflight from `Origin: https://bridge.cl8y.com` to `/api/v1/signatures/status` returns `Access-Control-Allow-Origin: https://bridge.cl8y.com`. - [ ] **AC4.** Portal allowlist includes `https://bridge.cl8y.com` so post-sign navigation can return to the bridge (rebuild done). Production portal still rejects unlisted `redirect_uri`. - [ ] **AC5.** CL8Y-web sibling: no `TermsGate` / no clickwrap dependency; exception documented in invariants/skill; optional Terms footer link is the constant `https://terms.cl8y.com` and is **not** named Privacy. - [ ] **AC6.** CL8Y-web does not call `/api/v1/signatures/status` (grep / e2e: no Legal API from the marketing origin). - [ ] **AC7.** Bridge frontend property string remains `bridge.cl8y.com` (no change unless a bug). #134 stays the frontend tracker. - [ ] **AC8.** No `ADMIN_TOKEN` added to any SPA. No `dangerouslySetInnerHTML` for terms HTML. No open redirect from the marketing Terms href. - [ ] **AC9.** Solana on the bridge remains fail-closed; this issue does not add a “skip Solana legal” flag. --- ## Test plan (functional paths) | # | Path | Expect | |---|------|--------| | T1 | `GET /api/v1/terms/latest?property=bridge.cl8y.com` | 200; `property` is `bridge.cl8y.com`; evm/terra/solana `sign_urls` present | | T2 | `GET /api/v1/terms/latest?property=dex.cl8y.com` | Unchanged (DEX still registered) | | T3 | OPTIONS status from `Origin: https://bridge.cl8y.com` | ACAO echo of that origin | | T4 | OPTIONS status from `Origin: https://evil.example` | No ACAO for attacker origin | | T5 | Portal sign URL with `redirect_uri=https://bridge.cl8y.com/…` | Allowed after rebuild | | T6 | Portal sign URL with `redirect_uri=https://phish.example/` | No auto-navigate (existing portal tests in `web/e2e/redirect.spec.ts` stay green) | | T7 | README / skill table | DEX yes, Bridge yes, cl8y.com exception | | T8 | CL8Y-web `/` | Homepage unchanged; Terms link (if added) opens `https://terms.cl8y.com` in a new tab via `ExternalLink` | | T9 | CL8Y-web unit tests | Terms URL constant; no clickwrap dep | | T10 | CL8Y-web Playwright (`--workers=5`) | Homepage CTAs still Bridge/DEX; reserved legal-guess paths remain #12’s job (do not 200 a fake Privacy page from this ticket) | | T11 | Bridge e2e `clickwrap.spec.ts` | Still green with mocked Legal; production round-trip is #134 verify after AC2–AC4 | | T12 | Marketing `GET https://cl8y.com/` (or preview) | No redirect to `/sign/evm`; no TermsGate overlay | | T13 | `GET …/terms/latest?property=cl8y.com` | May upsert/exist historically; **must not** be treated as “marketing is gated” in the matrix | Legal portal Playwright: existing `web/e2e` suite, 5 workers as already configured. No new wallet-sign e2e required in CL8Y-web. --- ## Test plan (attack, hack, and abuse) | # | Vector | Expect | |---|--------|--------| | A1 | Use a `cl8y.com` signature as evidence on the bridge | API keys by property; bridge client sends `bridge.cl8y.com` only (`clickwrap.ts` unit tests already pin this) | | A2 | Derive bridge property from `window.location.hostname` / Host header spoof | Forbidden; constant `BRIDGE_CLICKWRAP_PROPERTY` | | A3 | Mount `TermsGate` on CL8Y-web with a hardcoded dummy account | Forbidden by exception + grep AC6 | | A4 | localStorage / cookie `signed_latest=true` on cl8y.com | Must not exist; DEX/bridge must not read marketing storage | | A5 | Query `?signed=1` / `?property=dex.cl8y.com` on marketing | Ignored; no status client | | A6 | Footer Terms `href` from `?next=` / `javascript:` / `//evil` | Impossible; compile-time `https://terms.cl8y.com` | | A7 | Open redirect via portal `redirect_uri` crafted by marketing | Marketing must not call `buildSignUrl` | | A8 | CORS `*` to “make bridge work” | Reject; exact origin only | | A9 | Add `https://cl8y.com` to CORS so a future silent status poll can run | Out of policy; exception says **no** status poll from marketing | | A10 | Skip Solana legal on bridge because portal envelope gap exists | Fail closed; document in #134, do not add skip env here | | A11 | XSS via terms HTML in the marketing footer | Text link only; no innerHTML of API content | | A12 | Clickjack the portal from cl8y.com | Do not iframe `terms.cl8y.com`; `ExternalLink` / top navigation. Marketing already intends `frame-ancestors 'none'` (CL8Y-web host-headers skill) | | A13 | Phishing clone of cl8y.com that points SDK at an attacker API | N/A if no SDK; if someone adds it later, pin SDK defaults like DEX/bridge | | A14 | Register `www.cl8y.com` and claim parity | Apex marketing exception applies to the served marketing origin; do not invent extra gated properties without an integrator | | A15 | Wagmi `reconnectOnMount` supplies an `0x` and someone wires TermsGate “because the provider exists” | Explicitly banned; leftover wagmi is not a clickwrap account source | --- ## Verification criteria 1. **Docs:** `skills/integrator-coverage/SKILL.md` exists; `skills/README.md` links it; README API examples use a gated dapp property (`dex.cl8y.com`), not “marketing is gated.” 2. **API:** curls in T1 and T3 pass from a public network (no admin token in logs). 3. **Portal:** allowlist rebuild confirmed by a bridge `redirect_uri` success path **or** by reading the built allowlist constant in the portal bundle (string `https://bridge.cl8y.com` present; attacker origin absent). 4. **CL8Y-web sibling:** `yarn test`, `yarn typecheck`, `yarn test:e2e --workers=5` green; no clickwrap in `package.json`; footer Terms (if present) is the constant. 5. **Bridge:** no required code change. After AC2–AC4, #134 manual unsigned → portal → return → deposit CTA can be verified on that issue. 6. **Grep:** `TermsGate` / `cl8y-clickwrap` absent under `code/CL8Y-web/src`. --- ## Out of scope - Re-wiring `BridgeTermsGate` / DEX `ConnectedTermsGate`. - Authoring a Privacy Notice, cookie banner, or CMP (CL8Y-web#12 and bridge#165). - Publishing `@plasticdigits/cl8y-clickwrap` to npmjs ([#14](https://git.cl8y.com/code/cl8y-ecosystem-legal/issues/14)) — integrators already consume `0.1.1`. - yieldomega.com / other hosts not in the table (add rows in a new issue if they grow a wallet UI). - Telegram clickwrap on web properties. - Changing Legal message format / Content-SHA256 oracle. - Removing unused wagmi from CL8Y-web (separate cleanup). ## References - DEX TermsGate: [cl8y-dex-terraclassic#517](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/517) - Bridge TermsGate frontend: [cl8y-bridge-monorepo#134](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/134) - Ops playbook: [#8](https://git.cl8y.com/code/cl8y-ecosystem-legal/issues/8), [#12](https://git.cl8y.com/code/cl8y-ecosystem-legal/issues/12) - SDK `TermsGate`: `packages/cl8y-clickwrap/src/react/TermsGate.tsx` - Portal / API: `https://terms.cl8y.com` · `https://api.terms.cl8y.com`
Author
Owner

cl8y-agent-control: queued implement job c8fa4ba5-33e2-4cb6-b010-4a977fa6fb1c (not executed; no Hetzner VM).

cl8y-agent-control: queued `implement` job `c8fa4ba5-33e2-4cb6-b010-4a977fa6fb1c` (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-ecosystem-legal#34
No description provided.