From b20564c4378bf3b61fc95e2b8d42f6c6b3d1c824 Mon Sep 17 00:00:00 2001 From: Tamas Date: Fri, 27 Feb 2026 12:27:12 +0100 Subject: [PATCH] fix: add runtime validation for ConnectionMode (WAPI-1129) ConnectionMode was a TypeScript-only type with no runtime enforcement. A malformed session request with an invalid mode (e.g. "admin") would silently fall through to the UntrustedConnectionHandler. Now both WalletClient and DappClient validate the mode at connect-time before any state transition, and throw a clear error for invalid values. Exports CONNECTION_MODES const and isValidConnectionMode() type guard from core for downstream consumers. --- packages/core/CHANGELOG.md | 4 ++++ packages/core/src/domain/connection-mode.ts | 7 ++++++- packages/core/src/index.ts | 2 +- packages/dapp-client/src/client.ts | 5 ++++- packages/wallet-client/src/client.ts | 5 ++++- 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index cfd009b..17b077b 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `isValidConnectionMode()` and `CONNECTION_MODES` for runtime validation of connection mode values + ### Changed - Add `validatePeerKey` method to `IKeyManager` interface for peer public key validation at handshake and resume time ([#70](https://github.com/MetaMask/mobile-wallet-protocol/pull/70)) diff --git a/packages/core/src/domain/connection-mode.ts b/packages/core/src/domain/connection-mode.ts index 6697c90..fee6657 100644 --- a/packages/core/src/domain/connection-mode.ts +++ b/packages/core/src/domain/connection-mode.ts @@ -3,4 +3,9 @@ * 'trusted': A streamlined flow for same-device or trusted contexts that bypasses OTP. * 'untrusted': The high-security flow requiring user verification via OTP. */ -export type ConnectionMode = "trusted" | "untrusted"; +export const CONNECTION_MODES = ["trusted", "untrusted"] as const; +export type ConnectionMode = (typeof CONNECTION_MODES)[number]; + +export function isValidConnectionMode(value: unknown): value is ConnectionMode { + return typeof value === "string" && CONNECTION_MODES.includes(value as ConnectionMode); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7de7f7a..9a550b3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,6 @@ export { BaseClient } from "./base-client"; export { ClientState } from "./domain/client-state"; -export type { ConnectionMode } from "./domain/connection-mode"; +export { CONNECTION_MODES, type ConnectionMode, isValidConnectionMode } from "./domain/connection-mode"; export { CryptoError, ErrorCode, ProtocolError, SessionError, TransportError } from "./domain/errors"; export type { IKeyManager } from "./domain/key-manager"; export type { KeyPair } from "./domain/key-pair"; diff --git a/packages/dapp-client/src/client.ts b/packages/dapp-client/src/client.ts index d76c084..edca1e6 100644 --- a/packages/dapp-client/src/client.ts +++ b/packages/dapp-client/src/client.ts @@ -7,6 +7,7 @@ import { type IKeyManager, type ISessionStore, type ITransport, + isValidConnectionMode, type Message, type ProtocolMessage, type Session, @@ -107,9 +108,11 @@ export class DappClient extends BaseClient { */ public async connect(options: DappConnectOptions = {}): Promise { if (this.state !== ClientState.DISCONNECTED) throw new SessionError(ErrorCode.SESSION_INVALID_STATE, `Cannot connect when state is ${this.state}`); - this.state = ClientState.CONNECTING; const { mode = "untrusted", initialPayload } = options; + if (!isValidConnectionMode(mode)) throw new SessionError(ErrorCode.SESSION_INVALID_STATE, `Invalid connection mode: "${String(mode)}"`); + + this.state = ClientState.CONNECTING; const { pendingSession, request } = this._createPendingSessionAndRequest(mode, initialPayload); this.session = pendingSession; this.emit("session_request", request); diff --git a/packages/wallet-client/src/client.ts b/packages/wallet-client/src/client.ts index 1187a26..aa723bf 100644 --- a/packages/wallet-client/src/client.ts +++ b/packages/wallet-client/src/client.ts @@ -6,6 +6,7 @@ import { type IKeyManager, type ISessionStore, type ITransport, + isValidConnectionMode, type ProtocolMessage, type Session, SessionError, @@ -77,11 +78,13 @@ export class WalletClient extends BaseClient { */ public async connect(options: { sessionRequest: SessionRequest }): Promise { if (this.state !== ClientState.DISCONNECTED) throw new SessionError(ErrorCode.SESSION_INVALID_STATE, `Cannot connect when state is ${this.state}`); - this.state = ClientState.CONNECTING; const request = options.sessionRequest; + if (!isValidConnectionMode(request.mode)) throw new SessionError(ErrorCode.SESSION_INVALID_STATE, `Invalid connection mode: "${String(request.mode)}"`); if (Date.now() > request.expiresAt) throw new SessionError(ErrorCode.REQUEST_EXPIRED, "Session request expired"); + this.state = ClientState.CONNECTING; + const self = this; const context: IConnectionHandlerContext = { transport: this.transport,