You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
|`app`|`RouterLike`| No | — | Express-compatible app with `get`and `post` methods. REST routes are registered on it. Omit when using Next.js or another framework — call `createSession()`, `getSession()`, and `sendRequest()` from your own route handlers instead. Uses a structural duck-type to avoid nominal type conflicts in monorepos. |
51
+
|`app`|`RouterLike`| No | — | Express-compatible app with `get`, `post`, and `delete` methods. REST routes are registered on it. Omit when using Next.js or another framework — call `createSession()`, `getSession()`, `sendRequest()`, and `deleteSession()` from your own route handlers instead. Uses a structural duck-type to avoid nominal type conflicts in monorepos. |
50
52
|`server`|`http.Server`|**Yes**| — | HTTP server. The WebSocket upgrade handler is attached here. |
51
53
|`wallet`|`WalletLike`|**Yes**| — | Backend wallet for encrypting/decrypting messages. Use `ProtoWallet` with a stable private key: `new ProtoWallet(PrivateKey.fromHex(process.env.WALLET_PRIVATE_KEY!))`. The same key must be used across restarts — the mobile derives its ECDH shared secret from the backend identity key embedded in the QR code. |
52
54
|`relayUrl`|`string`| No |`process.env.RELAY_URL` → `ws://localhost:3000`|`ws://` or `wss://` base URL of this server. Returned by `GET /api/session/:id` so the mobile can resolve it after scanning the QR. Not embedded in the QR itself. |
53
-
|`origin`|`string`| No |`process.env.ORIGIN` → `http://localhost:5173`|`http://` or `https://` URL of the backend API root. Used for WebSocket origin validation and embedded in the QR pairing URI. The mobile calls `{origin}/api/session/{topic}` over HTTPS to resolve the relay URL — this is the trust anchor. In production this is your app domain. In local dev with a split Vite/Node setup, set this to the backend's LAN address so the mobile device can reach it (see `MOBILE_ORIGIN` in the quickstart). |
55
+
|`origin`|`string`| No |`process.env.ORIGIN` → `http://localhost:5173`| Default `http://` or `https://` URL embedded in the QR pairing URI when `createSession()` is called without a per-session `origin` override. The mobile calls `{origin}/api/session/{topic}` over HTTPS to resolve the relay URL — this is the trust anchor. In production this is your app domain. For multi-app deployments (one relay shared by N webapps) leave this unset or set a sensible fallback, and pass `origin` per-call to `createSession({ origin })` instead. In local dev with a split Vite/Node setup, set this to the backend's LAN address so the mobile device can reach it (see `MOBILE_ORIGIN` in the quickstart). |
56
+
|`allowedOrigins`|`AllowedOrigins`| No |`origin` (legacy fallback) | Origin allowlist controlling (a) which origins may be claimed by callers of `createSession({ origin })`, and (b) which browser origins may open a desktop-role WebSocket. Accepts a `string`, `string[]`, `RegExp`, or `(origin: string) => boolean` predicate. When unset, falls back to the single-value `origin` for backward compatibility. Required for multi-app deployments. |
54
57
|`maxSessions`|`number`| No | unlimited | Maximum number of sessions held in memory at once. `GET /api/session` returns HTTP 429 when the limit is reached. |
55
58
|`schema`|`string`| No |`process.env.PAIRING_SCHEMA` → `'bsv-browser'`| Deep-link scheme used in the generated QR URI (without `://`). Defaults to `'bsv-browser'`. Set to your wallet's own scheme (e.g. `'bsv-browser'`, `'my-wallet'`) to target a specific app — the OS will open that app directly instead of showing a picker when multiple wallets are installed. The mobile app must register this scheme and pass it to `parsePairingUri` via `acceptedSchemas`. |
56
59
|`signQrCodes`|`boolean`| No |`true`| Sign the QR pairing URI with the backend wallet key. The mobile can verify the signature using `verifyPairingSignature` before connecting — this proves the QR fields have not been tampered with. Set to `false` only for backward compatibility with mobile apps that do not yet call `verifyPairingSignature`. |
@@ -59,10 +62,10 @@ new WalletRelayService(options: WalletRelayServiceOptions)
@@ -75,6 +78,8 @@ Creates a new pairing session, builds the `wallet://pair?…` URI, and generates
75
78
76
79
The `desktopToken` is a cryptographically random secret that must be passed as a `?token=` query parameter when the desktop opens its WebSocket connection (`role=desktop`). Keep it server-side — do not embed it in the QR code or share it with the mobile.
77
80
81
+
Pass `options.origin` to embed a per-session origin in the QR — used by multi-app deployments where the caller's URL (not the relay's) is the trust anchor. When omitted, the constructor `origin` is used. The built-in `GET /api/session` route forwards the request's `Origin` header automatically. If `allowedOrigins` is configured, a per-session origin that does not match throws (and the route responds with `403`).
82
+
78
83
---
79
84
80
85
**`getSession(id)`**
@@ -99,6 +104,20 @@ Encrypts an RPC call, sends it to the paired mobile wallet over WebSocket, and w
Terminates a session from the desktop side: closes the mobile's WebSocket (the mobile app is notified and transitions to `'disconnected'`), rejects any in-flight RPC requests immediately, and marks the session `'expired'`.
114
+
115
+
Throws `'Session not found'` if the session does not exist, or `'Invalid desktop token'` if the token does not match the one issued at session creation.
116
+
117
+
The primary use case is **logout**: call this when the user ends their app session so the mobile is notified immediately rather than waiting for the server-side TTL to expire. If you are using `useWalletRelayClient`, call `cancelSession()` instead — it calls `disconnect()` on the client, which sends this request automatically.
118
+
119
+
---
120
+
102
121
**`stop()`**
103
122
104
123
```ts
@@ -111,14 +130,19 @@ Stops the session GC timer and closes the WebSocket server. Call on process shut
|`DELETE`|`/api/session/:id`| Header: `X-Desktop-Token`|`204 No Content`|
139
+
140
+
`GET /api/session` automatically forwards the request's `Origin` header into `createSession({ origin })`, so the QR points back at the calling webapp rather than the relay's own URL. Returns `403` when the claimed origin is not in `allowedOrigins`, and `429` when `maxSessions` is reached.
119
141
120
142
`GET /api/session/:id` is called by the mobile app after scanning the QR to resolve the relay WebSocket URL. The mobile trusts this response because it is served over HTTPS from the origin embedded in the QR.
121
143
144
+
`DELETE /api/session/:id` closes the mobile's WebSocket connection server-side. The mobile app receives a close event and knows the session has ended. Returns `401` if the token is missing or wrong, `404` if the session does not exist.
145
+
122
146
---
123
147
124
148
### WalletRelayClient
@@ -225,13 +249,25 @@ try {
225
249
226
250
---
227
251
252
+
**`disconnect()`**
253
+
254
+
```ts
255
+
disconnect(): Promise<void>
256
+
```
257
+
258
+
Terminates the session server-side then cleans up locally. Sends `DELETE /api/session/:id` with the desktop token so the backend closes the mobile's WebSocket and marks the session expired. Errors from the server call are swallowed — local teardown always completes.
259
+
260
+
Prefer `disconnect()` over `destroy()` when you want the mobile app to be notified that the session has ended (for example, on user logout). Falls back to `destroy()`-only behaviour if there is no active session or desktop token.
261
+
262
+
---
263
+
228
264
**`destroy()`**
229
265
230
266
```ts
231
267
destroy(): void
232
268
```
233
269
234
-
Stops the polling interval. Call on component unmount or teardown.
270
+
Stops the polling interval and clears the desktop token. Local cleanup only — the server session remains active and the mobile is not notified. Prefer `disconnect()` when the mobile should be notified.
React hook wrapping [`WalletRelayClient`](#walletrelayclient) with React state. The primary integration point for web apps — replaces the scaffolded `useWalletSession` template hook.
|`sessionStorageKey`|`string`|`'wallet-relay-session:<apiUrl>'`| Override the storage key. |
462
498
|`sessionStorageTtl`|`number`|`86400000`| Max age (ms) before a persisted session is discarded client-side. |
463
499
|`autoCreate`|`boolean`|`true`| When `true`, `resumeSession()` is tried on mount, falling back to `createSession()` if nothing to resume. Set to `false` to control timing manually. |
500
+
|`autoResume`|`boolean`|`false`| When `true`*and*`autoCreate` is `false`, attempts `resumeSession()` on mount but never auto-creates. Useful when a "Sign in with phone" button owns session creation while you still want refreshes to keep the user paired. No effect when `autoCreate !== false` — resume is already part of that path. |
|`error`|`string \| null`| Error from the last failed `createSession()`, or `null`. |
472
509
|`createSession`|`() => Promise<SessionInfo>`| Create a new session and restart polling. Safe to call multiple times — replaces the existing session. |
510
+
|`resumeSession`|`() => Promise<SessionInfo \| null>`| Try to resume a persisted session from `sessionStorage`. Returns the resumed `SessionInfo` (which exposes the `wallet` proxy when status is `'connected'`), or `null` if nothing to resume or the server says it's expired. Concurrent calls are deduped — the second caller gets the in-flight promise. |
511
+
|`cancelSession`|`() => void`| Resets all state to `null`, then calls `disconnect()` on the client (fire-and-forget). This terminates the session server-side and closes the mobile's WebSocket so the mobile app is notified. Call this on unmount when leaving a QR page, or on user logout. A subsequent `createSession()` starts fresh. |
473
512
|`sendRequest`|`(method: string, params?: unknown) => Promise<WalletResponse>`| Send an RPC call to the paired mobile. Throws if no session is active. |
474
513
|`wallet`|`Pick<WalletInterface, WalletMethodName> \| null`| Drop-in `WalletInterface` proxy when connected, `null` otherwise. See [wallet proxy](#wallet-proxy). |
475
514
@@ -734,12 +773,20 @@ Topic-keyed WebSocket bridge. Mounts at `/ws`. Connections use `?topic=<sessionI
|`allowedOrigin`|`string`| If set, incoming WebSocket connections that include an `Origin` header must match this value exactly or the connection is rejected with close code `1008`. Browser clients always send `Origin` and cannot spoof it. Native clients (React Native, server-to-server) omit the header and are exempt. |
786
+
|`allowedOrigins`|`AllowedOrigins`| Origin allowlist for browser WS upgrades (`role=desktop`). Accepts a `string`, `string[]`, `RegExp`, or `(origin: string) => boolean` predicate. Connections whose `Origin` header does not match are rejected with close code `1008`. Native clients (React Native, server-to-server) omit `Origin` and are exempt. |
787
+
|`allowedOrigin`|`string`| Legacy single-value form, kept for backward compatibility. Equivalent to passing the same string as `allowedOrigins`. If both are set, `allowedOrigins` takes precedence. |
788
+
789
+
When neither option is set, no origin validation runs.
743
790
744
791
#### Methods
745
792
@@ -1105,6 +1152,20 @@ Converts between `number[]` byte arrays and base64url strings using `@bsv/sdk`'s
0 commit comments