Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONNECTIVITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,8 @@ The app is available for a new session when hub presence is not busy
backend not yet ready for play) and there is no consent prompt, reserved peer id,
buffered handshake, or live message handler (`isAvailableForNewSessionPrompt()`).
Backend readiness is owned by `InternalBlockchainInterface.isReadyForPlay()`
(simulator: connected; WalletConnect: a verified full-node peer, checked
privately). The app still connects to the hub normally while a backend is not
(simulator and Cloud Wallet: connected; WalletConnect: a verified full-node
peer, checked privately). The app still connects to the hub normally while a backend is not
ready; it simply advertises busy, and inbound `advisory_start` /
`session_proposal` must still be declined even if the game WebSocket is live. A
consent prompt is a temporary unavailable state for inbound matchmaking even
Expand Down
63 changes: 43 additions & 20 deletions FRONTEND_ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,9 @@ means session obligation, walletless (`shouldReportHubBusy`), or that the active
blockchain backend is not yet ready for play (`blockchainReady === false`, folded
into `shouldReportHubBusy` / `shouldReportHubBusyPresence`). Readiness is owned by
the backend behind `InternalBlockchainInterface.isReadyForPlay()` /
`onPlayReadinessChange()`: the simulator is ready whenever connected, while
WalletConnect polls privately for a verified full-node peer (peer count never
leaves the backend). The app still connects to the hub normally while a backend
`onPlayReadinessChange()`: the simulator and Cloud Wallet are ready whenever
connected, while WalletConnect polls privately for a verified full-node peer
(peer count never leaves the backend). The app still connects to the hub normally while a backend
is not ready; it just advertises busy. Shell mirrors the backend's readiness into
`blockchainReadyRef` via `onPlayReadinessChange`, and a wallet disconnect clears
it (the backend can no longer vouch for readiness). The `HubConnection` uses a
Expand Down Expand Up @@ -455,7 +455,7 @@ are grouped under those phase-owned payloads:
| `unreadGame` | `boolean?` | Whether the Game tab has unread activity. |
| `walletAlert` | `boolean?` | Whether the Wallet tab should show an alert dot. |
| `hubAlert` | `boolean?` | Whether the Hub tab should show an alert dot. |
| `blockchainType` | `'simulator' \| 'walletconnect'?` | Which wallet backend is active or should be reconnected. |
| `blockchainType` | `'simulator' \| 'walletconnect' \| 'cloud'?` | Which wallet backend is active or should be reconnected. |
| `serializedGameSession` | `Uint8Array?` | Raw binary WASM game-session state via `serialize()`. |
| `gameSessionSchemaVersion` | `bigint?` | Rust-owned schema ID for `serializedGameSession`; currently `6`. Missing or mismatched IDs are unsupported and cleared before deserialization. |
| `pairingToken` | `string?` | Locally generated identity for the current peer-session/controller instance. It is persisted so pre-cradle setup or a full session resumes into the same instance, and it correlates Shell transition completion with that instance; it is not protocol authority. |
Expand Down Expand Up @@ -1127,13 +1127,20 @@ Shell manages wallet connections through two abstractions defined in

- **`InternalBlockchainInterface`** — the backend-specific implementation
(`RealBlockchainInterface` for WalletConnect, `FakeBlockchainInterface` for
the simulator). Each exposes `beginConnect()`, `disconnect()`,
`isConnected()`, `spend()`, etc.
the simulator, `CloudBlockchainInterface` for Cloud Wallet OAuth). Each
exposes `beginConnect()`, `disconnect()`, `isConnected()`, `spend()`, etc.
- **`ConnectionSetup`** — returned by `beginConnect()`. Contains a `uri` for
the QR code and a `finalize()` promise that resolves when the wallet is
paired. Optionally contains `fields` (a map of input descriptors) indicating
the backend needs extra user input before connecting (e.g. the simulator's
initial balance).
the QR code and a `finalize(values?)` promise that resolves when the wallet is
paired. Optionally contains `fields` (a `Record` of typed input descriptors,
each `{ type: 'string' | 'bigint', label, default }`) indicating the backend
needs extra user input before connecting, plus an optional `title`/
`description` for the setup modal. Examples: the simulator's initial balance
(`bigint`), and Cloud Wallet's OAuth `clientId` / API URL / UI URL (`string`).
Cloud Wallet sets `skipQr: true` and completes OAuth inside `finalize()` after
persisting the entered config via `cloudWalletConfig.ts` (kept separate from
the OAuth tokens in `cloudWalletAuth.ts`). All OAuth/GraphQL calls resolve the
client id and endpoints at call time through `getCloudWallet*` getters, so
UI-entered config takes effect without a rebuild.

**Design principle:** Shell must not branch on `blockchainType` for connection
logic. All differences between backends live behind the interface. A single
Expand All @@ -1142,16 +1149,28 @@ and poll interval; the rest of the flow is generic.

**Connection lifecycle:**

1. User picks "Simulator" or "Link Wallet" → `handleConnect(bcType)`.
1. User picks "Simulator", "Link Wallet", or "Cloud Wallet" →
`handleConnect(bcType)`.
2. `handleConnect` calls `iface.beginConnect(uniqueId)`, which returns a
`ConnectionSetup`.
3. If `setup.fields` is present, Shell shows the `SimulatorSetupModal` overlay
so the user can provide the required values, then `handleFinalize()` calls
`setup.finalize()`.
4. If `setup.fields` is absent (WalletConnect), Shell renders the QR code and
immediately awaits `setup.finalize()`, which resolves when the wallet scans.
5. After finalize resolves, `completeConnection()` activates polling and
switches to the Hub tab.
3. If `setup.fields` is present, Shell shows the generic `ConnectionSetupModal`
overlay so the user can provide the required values, then `handleFinalize(values)`
calls `setup.finalize(values)`. This path is used by both the simulator and
Cloud Wallet (the latter is `skipQr` yet still collects OAuth config first).
4. If `setup.skipQr` is set with no fields (a restored WC/Cloud session), Shell
awaits `setup.finalize()` without showing a QR panel or modal.
5. If `setup.skipQr` is set *with* fields (Cloud Wallet, no stored auth), Shell
shows `ConnectionSetupModal` and does **not** call `finalize()` from silent
`handleConnect` or `performResume`. Auto-finalize would open an OAuth popup
or fail when no client id is configured; the user must submit the form (or
use an explicit Reconnect).
6. If `setup.fields` is absent and QR is required (WalletConnect pairing), Shell
renders the QR code and awaits `setup.finalize()`, which resolves when the
wallet scans.
7. After finalize resolves, `completeConnection()` activates polling and
switches to the Hub tab. Connect/finalize failures are surfaced on the Choose
Connection screen and inside the setup modal via `connectError`, rather than
silently resetting the chooser.

**Auto-reconnect:** Both backends implement their own WebSocket reconnect
following the shared connection discipline described in
Expand All @@ -1160,8 +1179,12 @@ Shell's `onConnectionChange` callback handles UI state
transitions (connected ↔ disconnected) generically. On page load, if the
user chooses to resume a pre-game save (one with `blockchainType` but no
`serializedGameSession`), Shell calls `handleConnect(bcType, true)` (silent mode)
to re-establish the connection automatically — no modals or QR codes are shown,
consistent with the principle that a reload should be invisible to the user.
to re-establish the connection automatically — no QR codes are shown, and the
simulator balance modal is skipped, consistent with the principle that a reload
should be invisible to the user. Cloud Wallet without stored auth is the
exception: `beginConnect` returns `skipQr` plus `fields`, so silent reconnect
and `performResume` keep `ConnectionSetupModal` (with a wallet alert) rather
than calling `finalize()` with no values.

**Session persistence:** `blockchainType` is written via
`saveSession({ blockchainType })` as soon as the wallet connection completes,
Expand Down
77 changes: 46 additions & 31 deletions desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ browser at `:3002`.

## Connection modes

The desktop build is **WalletConnect only**. The preload sets
The desktop build hides the local simulator. The preload sets
`window.__chiaDistribution = 'electron'`, and `front-end/src/util/distribution.ts`
uses it to hide the "Continue with Simulator" button and the simulator setup
modal, and to resume a saved session with no recorded `blockchainType` as
WalletConnect rather than simulator. The simulator remains available in the web
build.
uses it to hide the "Continue with Simulator" button and to resume a saved
session with no recorded `blockchainType` as WalletConnect rather than
simulator. WalletConnect and Cloud Wallet remain available. The simulator
stays in the web build.

The same flag suppresses the front end's multi-tab lease. That lease records its
owner in `localStorage` but identifies itself from `sessionStorage`, so a quit
Expand All @@ -59,15 +59,21 @@ public internet, `front-end/src/util/walletConnectMetadata.ts` substitutes a
public https identity when the page origin is not http(s) — the renderer origin
here is `chiagaming://app`, which no wallet can open or fetch.

Cloud Wallet OAuth uses that same custom-scheme origin as `redirect_uri`
(`chiagaming://app/oauth/callback`). The protocol handler serves the player
document at that path so the callback page can `postMessage` the authorization
code to the opener. The Cloud Wallet OAuth client must allow that redirect URI.

## Configuration

Optional JSON file at `<userData>/config.json`, where `<userData>` is
`~/Library/Application Support/Chia Gaming` on macOS,
`%APPDATA%\Chia Gaming` on Windows, and `~/.config/Chia Gaming` on Linux.

| Key | Default | Meaning |
| ------------ | ---------------------------------------------------- | ------------------------------------------- |
| `hubOrigins` | `["http://localhost:3003", "http://127.0.0.1:3003"]` | Hub origins the app may load and connect to |
| Key | Default | Meaning |
| -------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
| `hubOrigins` | `["http://localhost:3003", "http://127.0.0.1:3003"]` | Hub origins the app may load and connect to |
| `cloudWalletOrigins` | `["http://127.0.0.1:3000", "http://127.0.0.1:3001", "http://localhost:3000", "http://localhost:3001"]` | Cloud Wallet API and UI origins for OAuth, GraphQL, and approval popups |

Anything invalid is reported in an error dialog and the app exits rather than
starting with a half-applied policy.
Expand All @@ -77,26 +83,31 @@ is a starting point rather than a fixed set: a hub typed into the in-app picker
is added to it at runtime and written back to the file. See
[Hub trust](#hub-trust).

`cloudWalletOrigins` feeds `connect-src` and the popup allowlist. They are not
framed. A production Cloud Wallet is added here (and the OAuth client must
allow `chiagaming://app/oauth/callback` as a redirect URI). A hub grant writes
`hubOrigins` without dropping a `cloudWalletOrigins` key already in the file.

## Security posture

### Process isolation

The renderer has no Node.js reachable from it at all, and the IPC surface is a
single channel described under [Hub trust](#hub-trust).

| Setting | Value |
| ----------------------------- | ------- |
| Setting | Value |
| ----------------------------- | ------------------------------------------------------------------------- |
| `sandbox` | `true` (also `app.enableSandbox()`, which covers renderers created later) |
| `contextIsolation` | `true` |
| `nodeIntegration` | `false` |
| `nodeIntegrationInWorker` | `false` |
| `nodeIntegrationInSubFrames` | `false` |
| `webSecurity` | `true` |
| `allowRunningInsecureContent` | `false` |
| `experimentalFeatures` | `false` |
| `webviewTag` | `false` |
| `navigateOnDragDrop` | `false` |
| `devTools` | only in unpackaged builds |
| `contextIsolation` | `true` |
| `nodeIntegration` | `false` |
| `nodeIntegrationInWorker` | `false` |
| `nodeIntegrationInSubFrames` | `false` |
| `webSecurity` | `true` |
| `allowRunningInsecureContent` | `false` |
| `experimentalFeatures` | `false` |
| `webviewTag` | `false` |
| `navigateOnDragDrop` | `false` |
| `devTools` | only in unpackaged builds |

`src/preload/index.ts` exposes two things and nothing else: `__chiaDistribution`,
a string the front end reads during the first render to drop web-only
Expand Down Expand Up @@ -144,17 +155,17 @@ Three of those need explanation:
instead of reusing `front-end/public/index.html` — the browser entry point
bootstraps through an inline `<script>` that would need a hash or a nonce.
- `style-src` allows inline styles because Radix's scroll-lock injects a
`<style>` element at runtime. Inline *style* is not an XSS vector the way
inline *script* is.
`<style>` element at runtime. Inline _style_ is not an XSS vector the way
inline _script_ is.

### Network egress

`onBeforeRequest` cancels every `http`, `https`, `ws` and `wss` request whose
origin is not on the allowlist, and logs it. The allowlist is the configured hub
origins (plus their WebSocket forms) and the WalletConnect
endpoints `sign-client` actually reaches: the `.com` and `.org` relays, the
Verify API, and `pulse.walletconnect.org`. Requests on `chiagaming://` are
answered from disk and never touch the network stack.
origins (plus their WebSocket forms), the configured Cloud Wallet origins, and
the WalletConnect endpoints `sign-client` actually reaches: the `.com` and
`.org` relays, the Verify API, and `pulse.walletconnect.org`. Requests on
`chiagaming://` are answered from disk and never touch the network stack.

### Hub trust

Expand All @@ -177,11 +188,15 @@ is actually choosing.

### Navigation and permissions

- `setWindowOpenHandler` denies every `window.open`. The player app has no
external links, so nothing needs `shell.openExternal`.
- `will-frame-navigate` restricts the top frame to `chiagaming://app` and
sub-frames to the frame allowlist. It is used in preference to
`will-navigate`, which only sees the top frame.
- `setWindowOpenHandler` allows a popup only when its origin is on
`cloudWalletOrigins` (Cloud Wallet OAuth and funding approval). Those windows
get an empty preload so they cannot see `__chiaHub`. Every other `window.open`
is denied. About-window links still use `shell.openExternal` for the project
URL only.
- `will-frame-navigate` keeps the player window's top frame on `chiagaming://app`,
allows Cloud Wallet popups to reach `cloudWalletOrigins` and to return to the
app for `/oauth/callback`, and restricts sub-frames to the frame allowlist. It
is used in preference to `will-navigate`, which only sees the top frame.
- `will-attach-webview` is blocked, on top of `webviewTag: false`.
- Permission requests and checks are denied except `clipboard-sanitized-write`
from the app origin, which is what `navigator.clipboard.writeText` needs to
Expand Down
2 changes: 1 addition & 1 deletion desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"scripts": {
"typecheck": "tsc --project .",
"build:main": "esbuild src/main/index.ts --bundle --platform=node --target=node22 --format=cjs --external:electron --outfile=dist/main/index.cjs --sourcemap",
"build:preload": "esbuild src/preload/index.ts --bundle --platform=node --target=node22 --format=cjs --external:electron --outfile=dist/preload/index.cjs --sourcemap",
"build:preload": "esbuild src/preload/index.ts --bundle --platform=node --target=node22 --format=cjs --external:electron --outfile=dist/preload/index.cjs --sourcemap && esbuild src/preload/empty.ts --bundle --platform=node --target=node22 --format=cjs --external:electron --outfile=dist/preload/empty.cjs --sourcemap",
"stage": "node scripts/stage-renderer.mjs",
"build": "pnpm run typecheck && pnpm run build:main && pnpm run build:preload && pnpm run stage",
"start": "pnpm run build && electron .",
Expand Down
1 change: 1 addition & 0 deletions desktop/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="icon" href="favicon.svg" type="image/svg+xml" />
<base href="/" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Callback page scripts fail to load

High Severity

The desktop OAuth callback is served as index.html at chiagaming://app/oauth/callback, while scripts and styles stay relative (index.js, bootstrap.mjs). The new <base href="/" /> is ignored because CSP sets base-uri 'none', so those assets resolve under /oauth/ and 404. The callback bundle never runs, so no postMessage reaches the opener.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c82f6f7. Configure here.

<title>Chia Gaming</title>
<!--
The browser deploy resolves assets through a /build-meta.json fetch and an
Expand Down
Loading
Loading