import * as fs from "node:fs"; // IHttpServerAdapter is re-exported via the public barrel (`export * from './http'`) // but tsgo cannot resolve the chain. Use the dist subpath directly (type-only import). import type { IHttpServerAdapter } from "@microsoft/teams.apps/dist/http/index.js"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; import { formatUnknownError } from "./errors.js"; import type { MSTeamsAdapter } from "./messenger.js"; import type { MSTeamsCredentials, MSTeamsFederatedCredentials } from "./token.js"; import { buildUserAgent } from "./user-agent.js";
/** * Resolved Teams SDK modules loaded lazily to avoid importing when the * provider is disabled.
*/
export type MSTeamsTeamsSdk = {
App: typeofimport("@microsoft/teams.apps").App;
Client: typeofimport("@microsoft/teams.api").Client;
};
/** * A Teams SDK App instance used for token management and proactive messaging.
*/
export type MSTeamsApp = InstanceType<MSTeamsTeamsSdk["App"]>;
/** * Token provider compatible with the existing codebase, wrapping the Teams * SDK App's token methods.
*/
export type MSTeamsTokenProvider = {
getAccessToken: (scope: string) => Promise<string>;
};
type MSTeamsBotIdentity = {
id?: string;
name?: string;
};
/** * Create a no-op HTTP server adapter that satisfies the Teams SDK's * IHttpServerAdapter interface without spinning up an Express server. * * OpenClaw manages its own Express server for the Teams webhook endpoint, so * the SDK's built-in HTTP server is unnecessary. Passing this adapter via the * `httpServerAdapter` option prevents the SDK from creating the default * HttpPlugin (which uses the deprecated `plugins` array and registers an * Express middleware with the pattern `/api*` — invalid in Express 5). * * See: https://github.com/openclaw/openclaw/issues/55161 * See: https://github.com/openclaw/openclaw/issues/60732
*/ function createNoOpHttpServerAdapter(): IHttpServerAdapter { return {
registerRoute() {},
};
}
/** * Create a Teams SDK App instance from credentials. The App manages token * acquisition, JWT validation, and the HTTP server lifecycle. * * This replaces the previous CloudAdapter + MsalTokenProvider + authorizeJWT * from @microsoft/agents-hosting.
*/
export async function createMSTeamsApp(
creds: MSTeamsCredentials,
sdk: MSTeamsTeamsSdk,
): Promise<MSTeamsApp> { if (creds.type === "federated") { return createFederatedApp(creds, sdk);
} returnnew sdk.App({
clientId: creds.appId,
clientSecret: creds.appPassword,
tenantId: creds.tenantId,
httpServerAdapter: createNoOpHttpServerAdapter(),
} as ConstructorParameters<MSTeamsTeamsSdk["App"]>[0]);
}
function createFederatedApp(creds: MSTeamsFederatedCredentials, sdk: MSTeamsTeamsSdk): MSTeamsApp { if (creds.useManagedIdentity) { return createManagedIdentityApp(creds, sdk);
}
// Certificate-based auth if (!creds.certificatePath) { thrownew Error("Federated credentials require either a certificate path or managed identity.");
}
function createCertificateApp(
creds: MSTeamsFederatedCredentials,
privateKey: string,
sdk: MSTeamsTeamsSdk,
): MSTeamsApp { // Lazily create and cache the credential so the token cache is reused.
let credentialPromise: Promise<AzureTokenCredential> | null = null;
if (!token?.token) { thrownew Error("Failed to acquire token via certificate credential.");
}
return token.token;
};
returnnew sdk.App({
clientId: creds.appId,
tenantId: creds.tenantId,
token: tokenProvider,
httpServerAdapter: createNoOpHttpServerAdapter(),
} as unknown as ConstructorParameters<MSTeamsTeamsSdk["App"]>[0]);
}
function createManagedIdentityApp(
creds: MSTeamsFederatedCredentials,
sdk: MSTeamsTeamsSdk,
): MSTeamsApp { // Lazily create and cache the credential instance so the token cache is // reused across calls instead of hitting IMDS/AAD on every message.
let credentialPromise: Promise<AzureTokenCredential> | null = null;
const getCredential = async () => { if (!credentialPromise) {
credentialPromise = loadAzureIdentity().then((az) =>
creds.managedIdentityClientId
? new az.ManagedIdentityCredential(creds.managedIdentityClientId)
: new az.ManagedIdentityCredential(),
);
} return credentialPromise;
};
function normalizeOutboundActivity(textOrActivity: string | object): Record<string, unknown> { returntypeof textOrActivity === "string"
? ({ type: "message", text: textOrActivity } as Record<string, unknown>)
: (textOrActivity as Record<string, unknown>);
}
function createSendContext(params: {
sdk: MSTeamsTeamsSdk;
serviceUrl?: string;
conversationId?: string;
conversationType?: string;
bot?: MSTeamsBotIdentity;
replyToActivityId?: string;
getToken: () => Promise<string | undefined>;
treatInvokeResponseAsNoop?: boolean; /** * Azure AD tenant ID for the target conversation. Bot Framework requires this * on outbound proactive activities so the connector can route them to the * correct tenant. Missing `tenantId` causes HTTP 403 on proactive sends.
*/
tenantId?: string; /** Target user's Teams user ID (e.g. `29:xxx`); included on the recipient field for routing. */
recipientId?: string; /** Target user's Azure AD object ID; included as the recipient on personal DMs. */
recipientAadObjectId?: string;
}): MSTeamsSendContext { const apiClient =
params.serviceUrl && params.conversationId
? createApiClient(params.sdk, params.serviceUrl, params.getToken)
: undefined;
/** * Build a CloudAdapter-compatible adapter using the Teams SDK REST client. * * This replaces the previous CloudAdapter from @microsoft/agents-hosting. * For incoming requests: the App's HTTP server handles JWT validation. * For proactive sends: uses the Bot Framework REST API via * @microsoft/teams.api Client.
*/
export function createMSTeamsAdapter(app: MSTeamsApp, sdk: MSTeamsTeamsSdk): MSTeamsAdapter { return {
async continueConversation(_appId, reference, logic) { const serviceUrl = reference.serviceUrl; if (!serviceUrl) { thrownew Error("Missing serviceUrl in conversation reference");
}
const conversationId = reference.conversation?.id; if (!conversationId) { thrownew Error("Missing conversation.id in conversation reference");
}
// Bot Framework requires `tenantId` on proactive sends so the connector // can route them to the correct Azure AD tenant. Without it, requests // fail with HTTP 403. Prefer the top-level `reference.tenantId` (captured // from `activity.channelData.tenant.id` at inbound time) and fall back // to `conversation.tenantId` for older stored references. const tenantId = reference.tenantId ?? reference.conversation?.tenantId; const recipientAadObjectId = reference.aadObjectId ?? reference.user?.aadObjectId;
// For invoke activities, send HTTP 200 immediately before running // handler logic so slow operations (file uploads, reflections) don't // hit Teams invoke timeouts ("unable to reach app"). if (isInvoke) {
response.status(200).send();
}
await logic(context);
if (!isInvoke) {
response.status(200).send();
}
} catch (err) { if (!isInvoke) {
response.status(500).send({ error: formatUnknownError(err) });
}
}
},
async updateActivity(_context, _activity) { // No-op: updateActivity is handled via REST in streaming-message.ts
},
async deleteActivity(_context, _reference) { // No-op: deleteActivity not yet implemented for Teams SDK adapter
},
};
}
/** * Bot Framework issuer → JWKS mapping. * During Microsoft's transition, inbound service tokens can be signed by either * the legacy Bot Framework issuer or the Entra issuer. Each gets its own JWKS * endpoint so we verify signatures with the correct key set.
*/ const BOT_FRAMEWORK_ISSUERS: ReadonlyArray<{
issuer: string | ((tenantId: string) => string);
jwksUri: string;
}> = [
{
issuer: "https://api.botframework.com",
jwksUri: "https://login.botframework.com/v1/.well-known/keys",
},
{
issuer: (tenantId: string) => `https://login.microsoftonline.com/${tenantId}/v2.0`,
jwksUri: "https://login.microsoftonline.com/common/discovery/v2.0/keys",
},
{ // SingleTenant bot deployments (Microsoft's default since 2025-07-31) get // tokens signed by the Azure AD v1 endpoint, whose issuer is scoped to the // bot's tenant. This must be a function so each deployment accepts its own // tenant rather than a single hardcoded one (#64270).
issuer: (tenantId: string) => `https://sts.windows.net/${tenantId}/`,
jwksUri: "https://login.microsoftonline.com/common/discovery/v2.0/keys",
},
];
type BotFrameworkJwtDeps = {
jwt: typeofimport("jsonwebtoken");
JwksClient: typeofimport("jwks-rsa").JwksClient;
};
// Decode without verification to extract issuer and kid for key lookup. const header = decodeHeader(token); const unverifiedPayload = jwt.decode(token); if (
!header?.kid ||
!isJwtPayloadObject(unverifiedPayload) || typeof unverifiedPayload.iss !== "string"
) { returnfalse;
}
// Resolve which JWKS endpoint to use based on the issuer claim. const issuerEntry = resolveIssuerEntry(unverifiedPayload.iss); if (!issuerEntry) { returnfalse;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.