fix(msteams): prevent path-to-regexp crash with express 5 (#55440)

Fixes #55250, #54960, #54889, #54852, #54703

thanks @lml2468
This commit is contained in:
Menglin Li
2026-04-01 23:00:06 +08:00
committed by GitHub
parent 181aef5cbe
commit d5554491a8
2 changed files with 97 additions and 3 deletions

View File

@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import {
createBotFrameworkJwtValidator,
createMSTeamsAdapter,
createMSTeamsApp,
type MSTeamsTeamsSdk,
} from "./sdk.js";
import type { MSTeamsCredentials } from "./token.js";
@@ -70,6 +71,29 @@ function createSdkStub(): MSTeamsTeamsSdk {
};
}
describe("createMSTeamsApp", () => {
it("does not crash with express 5 path-to-regexp (#55161)", async () => {
// Regression test for: https://github.com/openclaw/openclaw/issues/55161
// The default HttpPlugin in @microsoft/teams.apps uses `express().use('/api*', ...)`
// which throws in express 5 (path-to-regexp v8+). createMSTeamsApp injects a no-op
// HTTP plugin stub to prevent the SDK from creating the default HttpPlugin.
const { App } = await import("@microsoft/teams.apps");
const { Client } = await import("@microsoft/teams.api");
const sdk: MSTeamsTeamsSdk = { App, Client };
const creds: MSTeamsCredentials = {
appId: "test-app-id",
appPassword: "test-secret",
tenantId: "test-tenant",
};
// This would throw "Missing parameter name at index 5: /api*" without the fix
const app = await createMSTeamsApp(creds, sdk);
expect(app).toBeDefined();
// Verify token methods are available (the reason we use the App class)
expect(typeof (app as unknown as Record<string, unknown>).getBotToken).toBe("function");
});
});
describe("createMSTeamsAdapter", () => {
it("provides deleteActivity in proactive continueConversation contexts", async () => {
const fetchMock = vi.fn(async () => new Response(null, { status: 204 }));

View File

@@ -53,6 +53,68 @@ export async function loadMSTeamsSdk(): Promise<MSTeamsTeamsSdk> {
};
}
/**
* Create a lightweight no-op HTTP plugin stub that satisfies the Teams SDK's
* plugin discovery by name ("http") but does NOT spin up an Express server.
*
* The default HttpPlugin in @microsoft/teams.apps registers an Express
* middleware with the pattern `/api*`. When the host application (OpenClaw)
* uses Express 5 — which depends on path-to-regexp v8 — that pattern is
* invalid and throws:
*
* Missing parameter name at index 5: /api*
*
* OpenClaw manages its own Express server for the Teams webhook endpoint, so
* the SDK's built-in HTTP server is unnecessary. Passing this stub prevents
* the SDK from creating the default HttpPlugin and avoids the crash.
*
* See: https://github.com/openclaw/openclaw/issues/55161
*/
async function createNoOpHttpPlugin(): Promise<unknown> {
// Lazy-import reflect-metadata (required by the Teams SDK decorator system)
// and the decorator key constants so we can tag the stub class correctly.
//
// FRAGILE: these are internal SDK paths (not public API). If
// @microsoft/teams.apps changes its dist layout, these imports will break.
// Pin the SDK version and re-verify after any upgrade.
await import("reflect-metadata");
const { PLUGIN_METADATA_KEY } =
await import("@microsoft/teams.apps/dist/types/plugin/decorators/plugin.js");
const { PLUGIN_DEPENDENCIES_METADATA_KEY } =
await import("@microsoft/teams.apps/dist/types/plugin/decorators/dependency.js");
const { PLUGIN_EVENTS_METADATA_KEY } =
await import("@microsoft/teams.apps/dist/types/plugin/decorators/event.js");
class NoOpHttpPlugin {
onInit() {}
async onStart() {}
onStop() {}
asServer() {
return {
onRequest: undefined as unknown,
initialize: async () => {},
start: async () => {},
stop: async () => {},
} as {
onRequest: unknown;
initialize: (opts?: unknown) => Promise<void>;
start: (port?: number | string) => Promise<void>;
stop: () => Promise<void>;
};
}
}
Reflect.defineMetadata(
PLUGIN_METADATA_KEY,
{ name: "http", version: "0.0.0", description: "no-op stub (express 5 compat)" },
NoOpHttpPlugin,
);
Reflect.defineMetadata(PLUGIN_DEPENDENCIES_METADATA_KEY, [], NoOpHttpPlugin);
Reflect.defineMetadata(PLUGIN_EVENTS_METADATA_KEY, [], NoOpHttpPlugin);
return new NoOpHttpPlugin();
}
/**
* Create a Teams SDK App instance from credentials. The App manages token
* acquisition, JWT validation, and the HTTP server lifecycle.
@@ -60,12 +122,20 @@ export async function loadMSTeamsSdk(): Promise<MSTeamsTeamsSdk> {
* This replaces the previous CloudAdapter + MsalTokenProvider + authorizeJWT
* from @microsoft/agents-hosting.
*/
export function createMSTeamsApp(creds: MSTeamsCredentials, sdk: MSTeamsTeamsSdk): MSTeamsApp {
export async function createMSTeamsApp(
creds: MSTeamsCredentials,
sdk: MSTeamsTeamsSdk,
): Promise<MSTeamsApp> {
const noOpHttp = await createNoOpHttpPlugin();
// Use type assertion: the SDK's AppOptions generic narrows `plugins` to
// Array<TPlugin>, but our no-op stub satisfies the runtime contract without
// matching the decorator-heavy IPlugin type at compile time.
return new sdk.App({
clientId: creds.appId,
clientSecret: creds.appPassword,
tenantId: creds.tenantId,
});
plugins: [noOpHttp],
} as ConstructorParameters<MSTeamsTeamsSdk["App"]>[0]);
}
/**
@@ -396,7 +466,7 @@ export function createMSTeamsAdapter(app: MSTeamsApp, sdk: MSTeamsTeamsSdk): MST
export async function loadMSTeamsSdkWithAuth(creds: MSTeamsCredentials) {
const sdk = await loadMSTeamsSdk();
const app = createMSTeamsApp(creds, sdk);
const app = await createMSTeamsApp(creds, sdk);
return { sdk, app };
}