fix: accept legacy client IDs (clawdbot-*, moltbot-*) for backward compatibility
Fixes #4590 During the rebranding from 'clawdbot' to 'moltbot' to 'openclaw', companion apps were updated to send new client IDs ('openclaw-macos', 'openclaw-ios', etc.), but older gateway versions still required the legacy IDs in the validation schema. This caused WebSocket handshake failures when connecting companion apps in Direct mode to older gateways, particularly when proxied via Tailscale Serve. Changes: - Add LEGACY_GATEWAY_CLIENT_IDS const with both clawdbot-* and moltbot-* IDs - Merge legacy IDs into ALL_GATEWAY_CLIENT_IDS for validation - Update GatewayClientIdSchema to accept all legacy + current IDs - Add e2e tests to verify legacy client IDs are accepted - Preserve backward compatibility while allowing gradual rollout This allows companion apps and gateways to be upgraded independently without breaking existing connections.
This commit is contained in:
parent
da71eaebd2
commit
a9be84ad24
@ -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,9 @@ export type GatewayClientInfo = {
|
||||
instanceId?: string;
|
||||
};
|
||||
|
||||
const GATEWAY_CLIENT_ID_SET = new Set<GatewayClientId>(Object.values(GATEWAY_CLIENT_IDS));
|
||||
const GATEWAY_CLIENT_ID_SET = new Set<GatewayClientId>(
|
||||
Object.values(ALL_GATEWAY_CLIENT_IDS),
|
||||
);
|
||||
const GATEWAY_CLIENT_MODE_SET = new Set<GatewayClientMode>(Object.values(GATEWAY_CLIENT_MODES));
|
||||
|
||||
export function normalizeGatewayClientId(raw?: string | null): GatewayClientId | undefined {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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<void>((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<void>((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<void>((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();
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user