Skip to content

Commit d555449

Browse files
authored
fix(msteams): prevent path-to-regexp crash with express 5 (#55440)
Fixes #55250, #54960, #54889, #54852, #54703 thanks @lml2468
1 parent 181aef5 commit d555449

File tree

2 files changed

+97
-3
lines changed

2 files changed

+97
-3
lines changed

extensions/msteams/src/sdk.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
22
import {
33
createBotFrameworkJwtValidator,
44
createMSTeamsAdapter,
5+
createMSTeamsApp,
56
type MSTeamsTeamsSdk,
67
} from "./sdk.js";
78
import type { MSTeamsCredentials } from "./token.js";
@@ -70,6 +71,29 @@ function createSdkStub(): MSTeamsTeamsSdk {
7071
};
7172
}
7273

74+
describe("createMSTeamsApp", () => {
75+
it("does not crash with express 5 path-to-regexp (#55161)", async () => {
76+
// Regression test for: https://github.com/openclaw/openclaw/issues/55161
77+
// The default HttpPlugin in @microsoft/teams.apps uses `express().use('/api*', ...)`
78+
// which throws in express 5 (path-to-regexp v8+). createMSTeamsApp injects a no-op
79+
// HTTP plugin stub to prevent the SDK from creating the default HttpPlugin.
80+
const { App } = await import("@microsoft/teams.apps");
81+
const { Client } = await import("@microsoft/teams.api");
82+
const sdk: MSTeamsTeamsSdk = { App, Client };
83+
const creds: MSTeamsCredentials = {
84+
appId: "test-app-id",
85+
appPassword: "test-secret",
86+
tenantId: "test-tenant",
87+
};
88+
89+
// This would throw "Missing parameter name at index 5: /api*" without the fix
90+
const app = await createMSTeamsApp(creds, sdk);
91+
expect(app).toBeDefined();
92+
// Verify token methods are available (the reason we use the App class)
93+
expect(typeof (app as unknown as Record<string, unknown>).getBotToken).toBe("function");
94+
});
95+
});
96+
7397
describe("createMSTeamsAdapter", () => {
7498
it("provides deleteActivity in proactive continueConversation contexts", async () => {
7599
const fetchMock = vi.fn(async () => new Response(null, { status: 204 }));

extensions/msteams/src/sdk.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,19 +53,89 @@ export async function loadMSTeamsSdk(): Promise<MSTeamsTeamsSdk> {
5353
};
5454
}
5555

56+
/**
57+
* Create a lightweight no-op HTTP plugin stub that satisfies the Teams SDK's
58+
* plugin discovery by name ("http") but does NOT spin up an Express server.
59+
*
60+
* The default HttpPlugin in @microsoft/teams.apps registers an Express
61+
* middleware with the pattern `/api*`. When the host application (OpenClaw)
62+
* uses Express 5 — which depends on path-to-regexp v8 — that pattern is
63+
* invalid and throws:
64+
*
65+
* Missing parameter name at index 5: /api*
66+
*
67+
* OpenClaw manages its own Express server for the Teams webhook endpoint, so
68+
* the SDK's built-in HTTP server is unnecessary. Passing this stub prevents
69+
* the SDK from creating the default HttpPlugin and avoids the crash.
70+
*
71+
* See: https://github.com/openclaw/openclaw/issues/55161
72+
*/
73+
async function createNoOpHttpPlugin(): Promise<unknown> {
74+
// Lazy-import reflect-metadata (required by the Teams SDK decorator system)
75+
// and the decorator key constants so we can tag the stub class correctly.
76+
//
77+
// FRAGILE: these are internal SDK paths (not public API). If
78+
// @microsoft/teams.apps changes its dist layout, these imports will break.
79+
// Pin the SDK version and re-verify after any upgrade.
80+
await import("reflect-metadata");
81+
const { PLUGIN_METADATA_KEY } =
82+
await import("@microsoft/teams.apps/dist/types/plugin/decorators/plugin.js");
83+
const { PLUGIN_DEPENDENCIES_METADATA_KEY } =
84+
await import("@microsoft/teams.apps/dist/types/plugin/decorators/dependency.js");
85+
const { PLUGIN_EVENTS_METADATA_KEY } =
86+
await import("@microsoft/teams.apps/dist/types/plugin/decorators/event.js");
87+
88+
class NoOpHttpPlugin {
89+
onInit() {}
90+
async onStart() {}
91+
onStop() {}
92+
asServer() {
93+
return {
94+
onRequest: undefined as unknown,
95+
initialize: async () => {},
96+
start: async () => {},
97+
stop: async () => {},
98+
} as {
99+
onRequest: unknown;
100+
initialize: (opts?: unknown) => Promise<void>;
101+
start: (port?: number | string) => Promise<void>;
102+
stop: () => Promise<void>;
103+
};
104+
}
105+
}
106+
107+
Reflect.defineMetadata(
108+
PLUGIN_METADATA_KEY,
109+
{ name: "http", version: "0.0.0", description: "no-op stub (express 5 compat)" },
110+
NoOpHttpPlugin,
111+
);
112+
Reflect.defineMetadata(PLUGIN_DEPENDENCIES_METADATA_KEY, [], NoOpHttpPlugin);
113+
Reflect.defineMetadata(PLUGIN_EVENTS_METADATA_KEY, [], NoOpHttpPlugin);
114+
115+
return new NoOpHttpPlugin();
116+
}
117+
56118
/**
57119
* Create a Teams SDK App instance from credentials. The App manages token
58120
* acquisition, JWT validation, and the HTTP server lifecycle.
59121
*
60122
* This replaces the previous CloudAdapter + MsalTokenProvider + authorizeJWT
61123
* from @microsoft/agents-hosting.
62124
*/
63-
export function createMSTeamsApp(creds: MSTeamsCredentials, sdk: MSTeamsTeamsSdk): MSTeamsApp {
125+
export async function createMSTeamsApp(
126+
creds: MSTeamsCredentials,
127+
sdk: MSTeamsTeamsSdk,
128+
): Promise<MSTeamsApp> {
129+
const noOpHttp = await createNoOpHttpPlugin();
130+
// Use type assertion: the SDK's AppOptions generic narrows `plugins` to
131+
// Array<TPlugin>, but our no-op stub satisfies the runtime contract without
132+
// matching the decorator-heavy IPlugin type at compile time.
64133
return new sdk.App({
65134
clientId: creds.appId,
66135
clientSecret: creds.appPassword,
67136
tenantId: creds.tenantId,
68-
});
137+
plugins: [noOpHttp],
138+
} as ConstructorParameters<MSTeamsTeamsSdk["App"]>[0]);
69139
}
70140

71141
/**
@@ -396,7 +466,7 @@ export function createMSTeamsAdapter(app: MSTeamsApp, sdk: MSTeamsTeamsSdk): MST
396466

397467
export async function loadMSTeamsSdkWithAuth(creds: MSTeamsCredentials) {
398468
const sdk = await loadMSTeamsSdk();
399-
const app = createMSTeamsApp(creds, sdk);
469+
const app = await createMSTeamsApp(creds, sdk);
400470
return { sdk, app };
401471
}
402472

0 commit comments

Comments
 (0)