diff --git a/src/gateway/protocol/client-info.ts b/src/gateway/protocol/client-info.ts index 9fc39ff11..1f9a48d48 100644 --- a/src/gateway/protocol/client-info.ts +++ b/src/gateway/protocol/client-info.ts @@ -13,7 +13,31 @@ export const GATEWAY_CLIENT_IDS = { PROBE: "openclaw-probe", } as const; -export type GatewayClientId = (typeof GATEWAY_CLIENT_IDS)[keyof typeof GATEWAY_CLIENT_IDS]; +// Legacy client IDs for backward compatibility +// These are deprecated but still accepted to prevent breakage during upgrades +export const LEGACY_GATEWAY_CLIENT_IDS = { + // Clawdbot era (pre-2026.1.29) + CLAWDBOT_CONTROL_UI: "clawdbot-control-ui", + CLAWDBOT_MACOS_APP: "clawdbot-macos", + CLAWDBOT_IOS_APP: "clawdbot-ios", + CLAWDBOT_ANDROID_APP: "clawdbot-android", + CLAWDBOT_PROBE: "clawdbot-probe", + // Moltbot era (intermediate rebrand) + MOLTBOT_CONTROL_UI: "moltbot-control-ui", + MOLTBOT_MACOS_APP: "moltbot-macos", + MOLTBOT_IOS_APP: "moltbot-ios", + MOLTBOT_ANDROID_APP: "moltbot-android", + MOLTBOT_PROBE: "moltbot-probe", +} as const; + +export const ALL_GATEWAY_CLIENT_IDS = { + ...GATEWAY_CLIENT_IDS, + ...LEGACY_GATEWAY_CLIENT_IDS, +} as const; + +export type GatewayClientId = + | (typeof GATEWAY_CLIENT_IDS)[keyof typeof GATEWAY_CLIENT_IDS] + | (typeof LEGACY_GATEWAY_CLIENT_IDS)[keyof typeof LEGACY_GATEWAY_CLIENT_IDS]; // Back-compat naming (internal): these values are IDs, not display names. export const GATEWAY_CLIENT_NAMES = GATEWAY_CLIENT_IDS; @@ -42,7 +66,7 @@ export type GatewayClientInfo = { instanceId?: string; }; -const GATEWAY_CLIENT_ID_SET = new Set(Object.values(GATEWAY_CLIENT_IDS)); +const GATEWAY_CLIENT_ID_SET = new Set(Object.values(ALL_GATEWAY_CLIENT_IDS)); const GATEWAY_CLIENT_MODE_SET = new Set(Object.values(GATEWAY_CLIENT_MODES)); export function normalizeGatewayClientId(raw?: string | null): GatewayClientId | undefined { diff --git a/src/gateway/protocol/schema/primitives.ts b/src/gateway/protocol/schema/primitives.ts index d43a16a1e..9e38c1dde 100644 --- a/src/gateway/protocol/schema/primitives.ts +++ b/src/gateway/protocol/schema/primitives.ts @@ -1,6 +1,6 @@ import { Type } from "@sinclair/typebox"; import { SESSION_LABEL_MAX_LENGTH } from "../../../sessions/session-label.js"; -import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../client-info.js"; +import { ALL_GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../client-info.js"; export const NonEmptyString = Type.String({ minLength: 1 }); export const SessionLabelString = Type.String({ @@ -9,7 +9,7 @@ export const SessionLabelString = Type.String({ }); export const GatewayClientIdSchema = Type.Union( - Object.values(GATEWAY_CLIENT_IDS).map((value) => Type.Literal(value)), + Object.values(ALL_GATEWAY_CLIENT_IDS).map((value) => Type.Literal(value)), ); export const GatewayClientModeSchema = Type.Union( diff --git a/src/gateway/server.ios-client-id.e2e.test.ts b/src/gateway/server.ios-client-id.e2e.test.ts index 3c00d23fb..d4f82adf5 100644 --- a/src/gateway/server.ios-client-id.e2e.test.ts +++ b/src/gateway/server.ios-client-id.e2e.test.ts @@ -91,3 +91,60 @@ test("accepts openclaw-android as a valid gateway client id", async () => { ws.close(); }); + +test("accepts legacy clawdbot-ios as a valid gateway client id (backward compat)", async () => { + const ws = new WebSocket(`ws://127.0.0.1:${port}`); + await new Promise((resolve) => ws.once("open", resolve)); + + const res = await connectReq(ws, { clientId: "clawdbot-ios", platform: "ios" }); + // We don't care if auth fails here; we only care that schema validation accepts the client id. + // A schema rejection would close the socket before sending a response. + if (!res.ok) { + // allow unauthorized error when gateway requires auth + // but reject schema validation errors + const message = String(res.error?.message ?? ""); + if (message.includes("invalid connect params")) { + throw new Error(message); + } + } + + ws.close(); +}); + +test("accepts legacy clawdbot-android as a valid gateway client id (backward compat)", async () => { + const ws = new WebSocket(`ws://127.0.0.1:${port}`); + await new Promise((resolve) => ws.once("open", resolve)); + + const res = await connectReq(ws, { clientId: "clawdbot-android", platform: "android" }); + // We don't care if auth fails here; we only care that schema validation accepts the client id. + // A schema rejection would close the socket before sending a response. + if (!res.ok) { + // allow unauthorized error when gateway requires auth + // but reject schema validation errors + const message = String(res.error?.message ?? ""); + if (message.includes("invalid connect params")) { + throw new Error(message); + } + } + + ws.close(); +}); + +test("accepts legacy moltbot-macos as a valid gateway client id (backward compat)", async () => { + const ws = new WebSocket(`ws://127.0.0.1:${port}`); + await new Promise((resolve) => ws.once("open", resolve)); + + const res = await connectReq(ws, { clientId: "moltbot-macos", platform: "macos" }); + // We don't care if auth fails here; we only care that schema validation accepts the client id. + // A schema rejection would close the socket before sending a response. + if (!res.ok) { + // allow unauthorized error when gateway requires auth + // but reject schema validation errors + const message = String(res.error?.message ?? ""); + if (message.includes("invalid connect params")) { + throw new Error(message); + } + } + + ws.close(); +});