openclaw - 💡(How to fix) Fix msteams: jwt.verify undefined under Node ESM-CJS interop — every Bot Framework activity silently rejected [3 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#74296Fetched 2026-04-30 06:26:01
View on GitHub
Comments
3
Participants
3
Timeline
6
Reactions
2
Author
Assignees
Timeline (top)
commented ×3assigned ×1closed ×1cross-referenced ×1

In extensions/msteams/src/sdk.ts:716, loadBotFrameworkJwtDeps does:

botFrameworkJwtDepsPromise ??= Promise.all([import("jsonwebtoken"), import("jwks-rsa")]).then(
  ([jwt, { JwksClient }]) => ({ jwt, JwksClient }),
);

jsonwebtoken is a CJS module. When dynamically imported under Node ESM, the namespace object's named-export synthesis is non-deterministic for CJS modules. On at least one Node version in production (we hit it on Node 24.13.1), the namespace exposes decode as a named export but not verify:

namespace.decode          → function ✓
namespace.verify          → undefined ✗
namespace.default.verify  → function ✓

So inside validate() (sdk.ts:777), jwt.verify(token, publicKey, …) throws TypeError: jwt.verify is not a function. The try/catch at sdk.ts:801 silently catches it and returns false — so every Bot Framework activity gets a 401 from the validator with no visible reason at any log level (the only failure log is log.debug?.(…) in monitor.ts, optional-chained, which no-ops if the child logger doesn't expose .debug).

In our deployment this manifested as 5 days, 22 hours of total Teams silence — Microsoft delivering activities every 1–2 minutes, every one rejected, with zero log noise. We had to patch dist/extensions/msteams/graph-users-Bgd5K8gB.js at runtime to get visibility into the rejection cause and prove the App Password was still valid by hitting MS's tenant-scoped token endpoint directly.

Error Message

The validator's silent-rejection behavior is a footgun. The try/catch at sdk.ts:801 returns false on any thrown error — including bugs like this one that aren't auth failures. Consider:

  1. Logging the caught error at warn (not debug?.()), at least when it's a TypeError / ReferenceError / similar developer-bug class.

Root Cause

Severe but silent. Any deployment whose Node version triggers the missing-named-export path is rendered Teams-deaf with no surface indication. We only noticed because a human noticed Toby had stopped replying.

Fix Action

Fix / Workaround

In our deployment this manifested as 5 days, 22 hours of total Teams silence — Microsoft delivering activities every 1–2 minutes, every one rejected, with zero log noise. We had to patch dist/extensions/msteams/graph-users-Bgd5K8gB.js at runtime to get visibility into the rejection cause and prove the App Password was still valid by hitting MS's tenant-scoped token endpoint directly.

Code Example

botFrameworkJwtDepsPromise ??= Promise.all([import("jsonwebtoken"), import("jwks-rsa")]).then(
  ([jwt, { JwksClient }]) => ({ jwt, JwksClient }),
);

---

namespace.decodefunctionnamespace.verifyundefinednamespace.default.verifyfunction
---

node --input-type=module -e "
const ns = await import('jsonwebtoken');
console.log('namespace.decode:', typeof ns.decode);
console.log('namespace.verify:', typeof ns.verify);
console.log('namespace.default.verify:', typeof ns.default?.verify);
"

---

namespace.decode: function
namespace.verify: undefined
namespace.default.verify: function

---

botFrameworkJwtDepsPromise ??= Promise.all([
  import("jsonwebtoken"),
  import("jwks-rsa"),
]).then(([jwtNs, { JwksClient }]) => ({
  // Node ESM-CJS interop for jsonwebtoken doesn't always synthesize
  // `verify` as a named export, only `decode`. Fall back to .default.
  jwt: (jwtNs && typeof jwtNs.verify === "function") ? jwtNs : (jwtNs?.default ?? jwtNs),
  JwksClient,
}));

---

import jwt from "jsonwebtoken";
// then no dynamic shape-check needed.
RAW_BUFFERClick to expand / collapse

Summary

In extensions/msteams/src/sdk.ts:716, loadBotFrameworkJwtDeps does:

botFrameworkJwtDepsPromise ??= Promise.all([import("jsonwebtoken"), import("jwks-rsa")]).then(
  ([jwt, { JwksClient }]) => ({ jwt, JwksClient }),
);

jsonwebtoken is a CJS module. When dynamically imported under Node ESM, the namespace object's named-export synthesis is non-deterministic for CJS modules. On at least one Node version in production (we hit it on Node 24.13.1), the namespace exposes decode as a named export but not verify:

namespace.decode          → function ✓
namespace.verify          → undefined ✗
namespace.default.verify  → function ✓

So inside validate() (sdk.ts:777), jwt.verify(token, publicKey, …) throws TypeError: jwt.verify is not a function. The try/catch at sdk.ts:801 silently catches it and returns false — so every Bot Framework activity gets a 401 from the validator with no visible reason at any log level (the only failure log is log.debug?.(…) in monitor.ts, optional-chained, which no-ops if the child logger doesn't expose .debug).

In our deployment this manifested as 5 days, 22 hours of total Teams silence — Microsoft delivering activities every 1–2 minutes, every one rejected, with zero log noise. We had to patch dist/extensions/msteams/graph-users-Bgd5K8gB.js at runtime to get visibility into the rejection cause and prove the App Password was still valid by hitting MS's tenant-scoped token endpoint directly.

Reproduction

node --input-type=module -e "
const ns = await import('jsonwebtoken');
console.log('namespace.decode:', typeof ns.decode);
console.log('namespace.verify:', typeof ns.verify);
console.log('namespace.default.verify:', typeof ns.default?.verify);
"

Output on the affected Node version:

namespace.decode: function
namespace.verify: undefined
namespace.default.verify: function

Proposed fix

Default-export-aware destructuring in loadBotFrameworkJwtDeps:

botFrameworkJwtDepsPromise ??= Promise.all([
  import("jsonwebtoken"),
  import("jwks-rsa"),
]).then(([jwtNs, { JwksClient }]) => ({
  // Node ESM-CJS interop for jsonwebtoken doesn't always synthesize
  // `verify` as a named export, only `decode`. Fall back to .default.
  jwt: (jwtNs && typeof jwtNs.verify === "function") ? jwtNs : (jwtNs?.default ?? jwtNs),
  JwksClient,
}));

Equivalent, uses static import:

import jwt from "jsonwebtoken";
// then no dynamic shape-check needed.

Either is fine; the static import is simpler if there isn't a load-deferral reason to keep the dynamic form.

Secondary observation (optional, separate fix)

The validator's silent-rejection behavior is a footgun. The try/catch at sdk.ts:801 returns false on any thrown error — including bugs like this one that aren't auth failures. Consider:

  1. Logging the caught error at warn (not debug?.()), at least when it's a TypeError / ReferenceError / similar developer-bug class.
  2. Adding the same safety to other dynamic CJS imports in the codebase (anywhere Promise.all([import("<cjs>"), …]) destructures named exports of a CJS package).

Environment

  • OpenClaw: v2026.4.5 (also reproduced against v2026.4.26 HEAD source — same code at extensions/msteams/src/sdk.ts:716)
  • Node: v24.13.1
  • jsonwebtoken: whatever the lockfile resolves (CJS package, default export pattern)
  • Bot type: SingleTenant
  • Symptom: every inbound POST /api/messages returns 401, no log lines, no signals captured

Severity

Severe but silent. Any deployment whose Node version triggers the missing-named-export path is rendered Teams-deaf with no surface indication. We only noticed because a human noticed Toby had stopped replying.

Happy to open the PR if helpful.

extent analysis

TL;DR

The proposed fix involves using default-export-aware destructuring in loadBotFrameworkJwtDeps to handle the non-deterministic namespace object's named-export synthesis for CJS modules under Node ESM.

Guidance

  • Apply the proposed fix by modifying loadBotFrameworkJwtDeps to use default-export-aware destructuring, as shown in the issue.
  • Consider using static import for jsonwebtoken if there's no need for dynamic loading.
  • Review other dynamic CJS imports in the codebase for similar issues and apply the same safety measures.
  • Improve error logging in the validator to prevent silent rejections and provide more visibility into errors.

Example

The proposed fix can be applied as follows:

botFrameworkJwtDepsPromise ??= Promise.all([
  import("jsonwebtoken"),
  import("jwks-rsa"),
]).then(([jwtNs, { JwksClient }]) => ({
  jwt: (jwtNs && typeof jwtNs.verify === "function") ? jwtNs : (jwtNs?.default ?? jwtNs),
  JwksClient,
}));

Alternatively, using static import:

import jwt from "jsonwebtoken";

Notes

The issue is specific to Node version v24.13.1 and may not affect other versions. The proposed fix should be reviewed and tested thoroughly to ensure it resolves the issue without introducing new problems.

Recommendation

Apply the proposed fix using default-export-aware destructuring to resolve the issue and improve error logging to prevent silent rejections. This approach provides a more robust solution and helps prevent similar issues in the future.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix msteams: jwt.verify undefined under Node ESM-CJS interop — every Bot Framework activity silently rejected [3 comments, 3 participants]