
Security News
Happy Birthday, Shai-Hulud
It has been one year since Shai-Hulud made its first appearance on npm.
@opentag/teams
Advanced tools
Microsoft Teams adapter primitives for OpenTag.
This package receives Microsoft Teams Bot Framework message activities,
normalizes supported messages into OpenTagEvents, verifies Bot Framework JWTs,
renders OpenTag delivery presentations, and posts replies back to the source Teams
conversation through the Bot Connector REST API. It is mounted by the
local-runtime dispatcher; this package does not run a standalone service by
itself, and Teams relay ingress is not currently supported.
apply 1 / reject 1 are routed back
to the originating OpenTag run.normalize.ts converts Teams activities into OpenTag events.auth.ts validates Bot Framework bearer tokens.token.ts obtains Connector API access tokens.connector.ts posts replies through the Bot Connector REST API.render.ts converts OpenTag presentations into Teams-friendly text.webhook-app.ts wires auth, normalization, dispatcher calls, and source-thread
replies for the /teams/messages endpoint.thread-key.ts builds stable OpenTag thread keys for Teams source threads.These details came from local simulation plus a real Teams smoke test against an Azure Bot and Microsoft 365 tenant.
auth.ts is intentionally fail-closed:
channelId must be msteams.serviceUrl claim is required and must match the Activity body
serviceUrl. OpenTag never trusts a body-controlled reply URL unless the
Bot Framework token binds the same URL.msteams) before the
request can create a run, submit an action, or trigger any outbound Connector
call.For local JWT simulation, OPENTAG_TEAMS_OPENID_METADATA_URL can point at a
local JWKS endpoint. Test JWKS keys must include endorsements: ["msteams"].
Production should use Microsoft metadata.
A channel activity can include a conversation id like:
19:<channel-id>@thread.tacv2;messageid=<activity-id>
The durable channel binding should use the base channel conversation id, not the reply-specific suffix:
19:<channel-id>@thread.tacv2
local-runtime also has a fallback that strips ;messageid=... so an incoming
full conversation id can still match a binding created with the base id.
Dispatcher source-thread lookup keeps both the full reply identity and a
canonical base-conversation alias, while the delivery presentation retains the full
conversation id needed to reply to the correct Teams thread.
The binding identity is effectively:
tenantIdTeams replies have their own activity.id. When a user replies apply 1 under
a proposal, the original proposal/root activity id is carried in
conversation.id as ;messageid=<root-activity-id>.
webhook-app.ts must route source-thread actions with the root activity id, not
the reply activity id. Otherwise dispatcher lookup cannot find the action
receipt and the user sees a generic failure for apply 1 / reject 1.
Before recording a decision, creating an ApplyPlan, or executing an adapter
mutation, Dispatcher revalidates the tenant/channel stored on the proposal and
requires the current Teams channel binding to retain the same identity and, for
repository actions, the same repository target. Removed, rebound, or
identity-incomplete bindings fail closed.
The Teams delivery adapter posts status updates back into the same channel thread. For proposal receipts, the rendered message should clearly indicate the target system of record, impact, preconditions, and exact Teams command to approve or reject the action.
Use pnpm directly in this repo; do not rely on corepack pnpm in local
maintainer shells where Corepack is unavailable.
Targeted regression tests:
pnpm vitest run \
packages/teams/test/auth.test.ts \
packages/teams/test/webhook-app.test.ts \
packages/local-runtime/test/dispatcher.teams.test.ts
Type checks:
pnpm --dir packages/teams exec tsc --noEmit
pnpm --dir packages/local-runtime exec tsc --noEmit
Local Bot Framework/JWKS simulation should cover normal @mention run creation,
apply action routing, forged-key rejection, and wrong-audience rejection. Keep
simulation helpers outside the published repo if they are only development
notebook artifacts.
| Symptom | Likely cause | What to inspect |
|---|---|---|
401 from /teams/messages | Wrong Microsoft App ID, wrong tenant/app secret, bad JWT metadata, or invalid audience | Bot App ID config, Azure Bot messaging endpoint, auth test fixtures, tunnel request body and headers |
| No request reaches local OpenTag | Teams app/bot not installed, bot not @mentioned, tunnel URL wrong, or dev tunnel requires auth | ngrok http://127.0.0.1:4040, devtunnel --allow-anonymous, Azure Bot Messaging endpoint |
| Channel binding not found | Binding used full conversation.id with ;messageid=... or wrong tenant/channel | Compare tenantId, channelData.channel.id, base conversation id, and OpenTag config |
apply 1 says action could not be processed | Action routed with reply activity id instead of proposal/root activity id, or apply credentials are missing | webhook-app.ts thread key derivation, dispatcher source-thread receipt, GitHub/GitLab apply config |
| Claude executor is not ready | Local Claude authentication is missing or invalid | Complete the local Claude login, restart OpenTag, and run opentag doctor |
User-facing setup is documented in:
docs/platforms/teams.en.mddocs/platforms/teams.zh-CN.mdKeep those guides in sync with package behavior whenever auth, binding, delivery, or action-routing semantics change.
FAQs
Microsoft Teams activity normalization and callback rendering for OpenTag.
We found that @opentag/teams demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.

Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.

Security News
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.