mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
fix(gateway): guard channel method descriptor projection
This commit is contained in:
58
src/gateway/channel-gateway-methods.test.ts
Normal file
58
src/gateway/channel-gateway-methods.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
channelGatewayMethodNamesInclude,
|
||||
listChannelGatewayMethodNames,
|
||||
} from "./channel-gateway-methods.js";
|
||||
|
||||
describe("channel gateway method projection", () => {
|
||||
it("keeps legacy names and descriptor names in registration order", () => {
|
||||
expect(
|
||||
listChannelGatewayMethodNames({
|
||||
gatewayMethods: ["legacy.start"],
|
||||
gatewayMethodDescriptors: [{ name: "descriptor.wait" }],
|
||||
}),
|
||||
).toEqual(["legacy.start", "descriptor.wait"]);
|
||||
});
|
||||
|
||||
it("ignores unreadable channel gateway descriptors", () => {
|
||||
const unreadableDescriptor = Object.defineProperty({}, "name", {
|
||||
get() {
|
||||
throw new Error("channel gateway descriptor name getter exploded");
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
listChannelGatewayMethodNames({
|
||||
gatewayMethods: ["legacy.start"],
|
||||
gatewayMethodDescriptors: [unreadableDescriptor, { name: "descriptor.wait" }],
|
||||
}),
|
||||
).toEqual(["legacy.start", "descriptor.wait"]);
|
||||
});
|
||||
|
||||
it("treats unreadable method arrays as absent", () => {
|
||||
const plugin = Object.defineProperty({}, "gatewayMethodDescriptors", {
|
||||
get() {
|
||||
throw new Error("channel gateway descriptors getter exploded");
|
||||
},
|
||||
});
|
||||
|
||||
expect(listChannelGatewayMethodNames(plugin)).toEqual([]);
|
||||
});
|
||||
|
||||
it("matches readable names without throwing on unreadable descriptors", () => {
|
||||
const unreadableDescriptor = Object.defineProperty({}, "name", {
|
||||
get() {
|
||||
throw new Error("channel gateway descriptor name getter exploded");
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
channelGatewayMethodNamesInclude(
|
||||
{
|
||||
gatewayMethodDescriptors: [unreadableDescriptor, { name: "web.login.start" }],
|
||||
},
|
||||
new Set(["web.login.start"]),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
53
src/gateway/channel-gateway-methods.ts
Normal file
53
src/gateway/channel-gateway-methods.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
type ChannelGatewayMethodDescriptorLike = {
|
||||
name?: unknown;
|
||||
};
|
||||
|
||||
function readMaybeArray<T>(
|
||||
plugin: unknown,
|
||||
key: "gatewayMethods" | "gatewayMethodDescriptors",
|
||||
): readonly T[] {
|
||||
if (!plugin || typeof plugin !== "object") {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const value = (plugin as Record<string, unknown>)[key];
|
||||
return Array.isArray(value) ? (value as readonly T[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function readDescriptorName(descriptor: ChannelGatewayMethodDescriptorLike): string | undefined {
|
||||
try {
|
||||
return typeof descriptor.name === "string" ? descriptor.name : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Lists readable channel-owned gateway method names without trusting plugin metadata accessors. */
|
||||
export function listChannelGatewayMethodNames(plugin: unknown): string[] {
|
||||
const methods: string[] = [];
|
||||
for (const method of readMaybeArray<unknown>(plugin, "gatewayMethods")) {
|
||||
if (typeof method === "string") {
|
||||
methods.push(method);
|
||||
}
|
||||
}
|
||||
for (const descriptor of readMaybeArray<ChannelGatewayMethodDescriptorLike>(
|
||||
plugin,
|
||||
"gatewayMethodDescriptors",
|
||||
)) {
|
||||
const name = readDescriptorName(descriptor);
|
||||
if (name !== undefined) {
|
||||
methods.push(name);
|
||||
}
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
|
||||
export function channelGatewayMethodNamesInclude(
|
||||
plugin: unknown,
|
||||
names: ReadonlySet<string>,
|
||||
): boolean {
|
||||
return listChannelGatewayMethodNames(plugin).some((method) => names.has(method));
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
import { listLoadedChannelPlugins } from "../channels/plugins/registry-loaded.js";
|
||||
import { listChannelGatewayMethodNames } from "./channel-gateway-methods.js";
|
||||
import { GATEWAY_EVENT_UPDATE_AVAILABLE } from "./events.js";
|
||||
import { listCoreAdvertisedGatewayMethodNames } from "./methods/core-descriptors.js";
|
||||
import { GATEWAY_AUX_METHODS } from "./server-aux-methods.js";
|
||||
|
||||
type GatewayMethodChannelPlugin = {
|
||||
gatewayMethods?: readonly string[];
|
||||
gatewayMethodDescriptors?: readonly { name: string }[];
|
||||
};
|
||||
|
||||
/** Lists core methods intentionally advertised to gateway clients. */
|
||||
export function listCoreGatewayMethods(): string[] {
|
||||
return listCoreAdvertisedGatewayMethodNames();
|
||||
@@ -15,13 +11,10 @@ export function listCoreGatewayMethods(): string[] {
|
||||
|
||||
function listChannelGatewayMethods(): string[] {
|
||||
const methods: string[] = [];
|
||||
for (const plugin of listLoadedChannelPlugins() as GatewayMethodChannelPlugin[]) {
|
||||
for (const plugin of listLoadedChannelPlugins()) {
|
||||
// Plugins may still expose legacy names while newer plugins expose descriptors.
|
||||
// Merge both so method discovery stays compatible during descriptor adoption.
|
||||
methods.push(...(plugin.gatewayMethods ?? []));
|
||||
for (const descriptor of plugin.gatewayMethodDescriptors ?? []) {
|
||||
methods.push(descriptor.name);
|
||||
}
|
||||
methods.push(...listChannelGatewayMethodNames(plugin));
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
|
||||
@@ -177,6 +177,55 @@ describe("webHandlers web.login.start", () => {
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores unreadable channel gateway descriptors when resolving the login provider", async () => {
|
||||
const loginWithQrStart = vi.fn().mockResolvedValue({
|
||||
connected: true,
|
||||
message: "connected",
|
||||
});
|
||||
const unreadableDescriptor = Object.defineProperty({}, "name", {
|
||||
get() {
|
||||
throw new Error("channel gateway descriptor name getter exploded");
|
||||
},
|
||||
});
|
||||
mocks.listChannelPlugins.mockReturnValue([
|
||||
{
|
||||
id: "broken-channel",
|
||||
gatewayMethodDescriptors: [unreadableDescriptor],
|
||||
gateway: { loginWithQrStart: vi.fn() },
|
||||
},
|
||||
{
|
||||
id: "whatsapp",
|
||||
gatewayMethodDescriptors: [{ name: "web.login.start" }],
|
||||
gateway: { loginWithQrStart },
|
||||
},
|
||||
]);
|
||||
const respond = vi.fn();
|
||||
|
||||
await webHandlers["web.login.start"](
|
||||
createOptions(
|
||||
{ accountId: "default" },
|
||||
{
|
||||
respond,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(loginWithQrStart).toHaveBeenCalledWith({
|
||||
accountId: "default",
|
||||
force: false,
|
||||
timeoutMs: undefined,
|
||||
verbose: false,
|
||||
});
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
true,
|
||||
{
|
||||
connected: true,
|
||||
message: "connected",
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("webHandlers web.login.wait", () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "../../../packages/gateway-protocol/src/index.js";
|
||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import type { ChannelId } from "../../channels/plugins/types.public.js";
|
||||
import { channelGatewayMethodNamesInclude } from "../channel-gateway-methods.js";
|
||||
import { formatForLog } from "../ws-log.js";
|
||||
import type { GatewayRequestHandlers, RespondFn } from "./types.js";
|
||||
import { assertValidParams } from "./validation.js";
|
||||
@@ -15,10 +16,7 @@ const WEB_LOGIN_METHODS = new Set(["web.login.start", "web.login.wait"]);
|
||||
/** Resolves the channel plugin that currently owns web QR-login methods. */
|
||||
const resolveWebLoginProvider = () =>
|
||||
listChannelPlugins().find((plugin) =>
|
||||
[
|
||||
...(plugin.gatewayMethods ?? []),
|
||||
...(plugin.gatewayMethodDescriptors ?? []).map((descriptor) => descriptor.name),
|
||||
].some((method) => WEB_LOGIN_METHODS.has(method)),
|
||||
channelGatewayMethodNamesInclude(plugin, WEB_LOGIN_METHODS),
|
||||
) ?? null;
|
||||
|
||||
type WebLoginProvider = NonNullable<ReturnType<typeof resolveWebLoginProvider>>;
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
} from "../secrets/runtime-state.js";
|
||||
import { createAuthRateLimiter, type AuthRateLimiter } from "./auth-rate-limit.js";
|
||||
import { resolveGatewayAuth } from "./auth.js";
|
||||
import { listChannelGatewayMethodNames } from "./channel-gateway-methods.js";
|
||||
import { ADMIN_SCOPE } from "./method-scopes.js";
|
||||
import {
|
||||
STARTUP_UNAVAILABLE_GATEWAY_METHODS,
|
||||
@@ -729,10 +730,7 @@ export async function startGatewayServer(
|
||||
const listStartupChannelGatewayMethods = () => {
|
||||
const methods: string[] = [];
|
||||
for (const plugin of listGatewayStartupChannelPlugins()) {
|
||||
methods.push(...(plugin.gatewayMethods ?? []));
|
||||
for (const descriptor of plugin.gatewayMethodDescriptors ?? []) {
|
||||
methods.push(descriptor.name);
|
||||
}
|
||||
methods.push(...listChannelGatewayMethodNames(plugin));
|
||||
}
|
||||
return methods;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user