feat: add @chat-adapter/zoom — Zoom Team Chat adapter#360
feat: add @chat-adapter/zoom — Zoom Team Chat adapter#360shishirsharma wants to merge 36 commits intovercel:mainfrom
Conversation
|
@shishirsharma is attempting to deploy a commit to the Vercel Labs Team on Vercel. A member of the Team first needs to authorize it. |
- Create packages/adapter-zoom/ with package.json, tsconfig.json, tsup.config.ts, vitest.config.ts modeled on adapter-whatsapp - Add @zoom/rivet@^0.4.0 as dependency (0.3.0 doesn't exist; 0.4.0 is latest) - Add .npmrc routing @zoom scope to public npm registry (Zoom internal Artifactory blocks tarball downloads in this environment) - pnpm install succeeds with all 19 workspace packages resolved [Rule 1 - Bug] @zoom/rivet@0.3.0 does not exist; updated to ^0.4.0 [Rule 3 - Blocker] Added .npmrc @zoom:registry override for npm access
- src/types.ts: export ZoomAdapterConfig, ZoomAdapterInternalConfig, ZoomCrcPayload, ZoomWebhookPayload with sorted interface members - src/index.ts: ZoomAdapter class stub implementing full Adapter interface with NotImplementedError for unimplemented methods; createZoomAdapter factory validates all 5 required credentials at construction time - src/index.test.ts: 14 it.todo() stubs covering WBHK-01/02/03 and AUTH-01/02/03/04 behaviors for Plans 02 and 03 to fill in - pnpm typecheck: exit 0, no errors - pnpm test: 14 todo, 0 failures
- WBHK-01: CRC challenge returns { plainToken, encryptedToken } with HTTP 200
- WBHK-02: tampered signature returns 401
- WBHK-02: missing signature returns 401
- WBHK-02: stale timestamp >5 minutes returns 401
- WBHK-03: valid signature passes (non-401 response)
…er (GREEN) - handleWebhook: captures raw body via request.text() before JSON.parse (WBHK-03) - CRC endpoint.url_validation handled BEFORE signature check (WBHK-01) - verifySignature: HMAC-SHA256 via timingSafeEqual, 5-minute staleness check (WBHK-02) - ZOOM-506645 mitigation: log body hex on timingSafeEqual length-mismatch throws - processEvent stub ready for Phase 2 event routing
- Add public getAccessToken() with in-memory cache (60s early-expiry buffer) - Use raw fetch with Basic auth for client_credentials grant - All 14 Phase 1 unit tests pass (5 webhook + 4 auth + 5 factory) - cachedToken private field with expiresAt comparison
- Fix import order and formatting in index.test.ts (ultracite check --write) - Add @zoom/rivet to knip ignoreDependencies (reserved for Phase 3 message sending) - pnpm validate exits 0: knip clean, check clean, all 14 zoom tests pass - dist/index.js (7KB) and dist/index.d.ts (3.2KB) built successfully
- 3 describe blocks covering toAst, fromAst, and round-trip scenarios - 17 it.todo stubs (no assertions yet — Plan 02 fills them in) - Zero test failures; Vitest reports all 17 as todo
…WBHK-05, THRD-01, THRD-02, THRD-03) - Add ZoomBotNotificationPayload, ZoomAppMentionPayload, ZoomThreadId types to types.ts - Implement encodeThreadId/decodeThreadId with ValidationError on bad input - Implement initialize() to store ChatInstance - Implement processEvent() routing: bot_notification and team_chat.app_mention handlers - Add DM thread reply limitation comment (ZOOM PLATFORM LIMITATION) - Add 10 new tests covering THRD-01, WBHK-04, WBHK-05, THRD-02, THRD-03 - Add @types/mdast devDependency for UnderlineNode type definition - All 24 tests pass, pnpm validate exits 0
…erage - New markdown.ts with ZoomFormatConverter extending BaseFormatConverter - toAst(): converts __underline__ (link-sentinel approach) and ~strikethrough~ (single→double tilde) to mdast nodes - fromAst(): converts underline node to __text__ (html inline), ~~strikethrough~~ post-processed to ~strikethrough~ - All 17 FMT-01/FMT-02/FMT-03 tests pass (replacing it.todo stubs) - Round-trips for __underline__ and ~strikethrough~ verified
- Add formatConverter field (ZoomFormatConverter) to ZoomAdapter class - Replace temporary parseMarkdown() calls in handleBotNotification and handleAppMention with this.formatConverter.toAst() - Implement renderFormatted() using this.formatConverter.fromAst() — no longer throws NotImplementedError - Remove parseMarkdown import (no longer needed); add Root type import - Fix type comparison for custom underline node in fromAst() to avoid TS2367 - All 41 tests pass, pnpm validate exits 0
…MSG-01, MSG-02) - Add describe block "ZoomAdapter — postMessage (MSG-01, MSG-02)" with 6 test cases - MSG-01-a/b/c: channel and DM routing, RawMessage return shape - MSG-02-a/b: replyTo adds reply_main_message_id; DM+replyTo logs THRD-03 warning - zoomFetch-error: non-2xx throws Error with operation and status code - All 6 tests fail RED with NotImplementedError (TDD phase confirmed)
…Chat API (MSG-01, MSG-02)
- Add private zoomFetch() that fetches Bearer token, sets headers, throws descriptive Error on non-2xx
- Replace postMessage() stub: calls POST /v2/chat/users/{robotJid}/messages with to_jid routing
- Support threaded replies via reply_main_message_id (MSG-02) with THRD-03 debug warning for DMs
- Return RawMessage with id (from message_id field), threadId, and raw Zoom API response
- All 30 tests pass; pnpm validate succeeds
…-03, MSG-04) - MSG-03: editMessage PATCH URL, Bearer auth, body, RawMessage return, error handling - MSG-04: deleteMessage DELETE URL, Bearer auth, void return, error handling
…-04)
- editMessage: PATCH /v2/chat/messages/{messageId} with message text, returns RawMessage
- deleteMessage: DELETE /v2/chat/messages/{messageId}, returns void
- No to_jid in edit/delete body per locked decision
- No body in DELETE options per Pitfall 4
- Move error regex patterns to top level to satisfy Biome useTopLevelRegex rule
- Prefix unused threadId param with _ in deleteMessage per Biome noUnusedParameters
…-tests - Add "@chat-adapter/zoom": "workspace:*" to integration-tests/package.json dependencies - Add "@chat-adapter/zoom" to VALID_PACKAGE_README_IMPORTS in documentation-test-utils.ts
- Create fixtures/replay/zoom/zoom.json with botNotification and appMention payloads - Create zoom-utils.ts with ZOOM_CREDENTIALS, createZoomWebhookRequest (HMAC v0:seconds:body), and setupZoomFetchMock - Create replay-zoom.test.ts with 4 passing tests for both event types (bot_notification and team_chat.app_mention)
- Documents Zoom Marketplace setup (Server-to-Server OAuth + Chatbot app) - Covers all env vars, required scopes, and configuration options - Includes Known Limitations for DM thread replies and Unicode HMAC bug - TypeScript code blocks validated by package-readmes.test.ts (16/16 pass)
- [Rule 1 - Bug] Add userName to ZoomAdapterConfig/ZoomAdapterInternalConfig and wire it in createZoomAdapter() factory with ZOOM_BOT_USERNAME env fallback - [Rule 3 - Blocking] Remove export from ZOOM_WEBHOOK_SECRET in zoom-utils.ts (knip unused) - [Rule 3 - Blocking] Extract /.*/ regex to top-level constant in replay-zoom.test.ts - [Rule 3 - Blocking] Apply ultracite formatter fixes to zoom-utils.ts and replay-zoom.test.ts
- Change line 118 from zoom:{userJid}:{userJid} to zoom:{userJid}:{event_ts}
- Matches implementation in index.ts:226-229 where messageId = String(eventTs)
- Add third describe block to replay-zoom.test.ts verifying thread.subscribe() stores state and onSubscribedMessage fires with the Zoom adapter - Use handleIncomingMessage() for follow-up to avoid dedup (Zoom assigns per-message thread IDs via event_ts; replay approach hits SDK deduplication) - Import Message class (not just type) to construct the follow-up message
…o types.ts - Export ZoomMessageWithReply interface with JSDoc (MSG-02 named cast target) - Add JSDoc to accountId in ZoomAdapterInternalConfig explaining why it is not sent in the S2S OAuth token request body
…eply in postMessage()
- Add ZoomMessageWithReply to import in index.ts; re-export for consumers
- Replace (message as { metadata?: { replyTo?: string } }) with named cast to ZoomMessageWithReply
- Update MSG-02-a and MSG-02-b tests to use ZoomMessageWithReply typed variable
- Auto-fix linting: sort interface members, format call site
- Remove @zoom/rivet from packages/adapter-zoom/package.json (Phase 3 used raw fetch instead) - Remove @zoom/rivet from knip.json ignoreDependencies - Update pnpm-lock.yaml - Fix trailing newline in .planning/config.json (Rule 3 - blocking issue for pnpm validate)
|
@shishirsharma This is great. Thanks. What is the chance that this is usable to join zoom calls? I looked at the API a while ago and it wasn't possible |
- Add isDM() method to ZoomAdapter for correct DM detection - Switch postMessage to /v2/im/chat/messages (client_credentials endpoint) - Handle channel-level thread IDs in decodeThreadId (no messageId) - Guard against empty text in postMessage (Zoom rejects empty messages) - Add renderPostable override to convert emoji placeholders to Unicode
- Add toZoomContentBody() to ZoomFormatConverter — converts mdast to Zoom's content body array format with per-segment bold/italic styles - postMessage now uses structured content body instead of markdown string - Heading blocks render as bold segments; list items get bullet prefix - Mixed bold/plain paragraphs render as plain text (Zoom limitation: each content.body item renders on its own line, no inline mixed styles) - Add renderPostable override with convertEmojiPlaceholders (Unicode)
373e875 to
16f51a8
Compare
…oints and update tests - editMessage: switch from PATCH /v2/chat/messages to PUT /v2/im/chat/messages (verified against @zoom/rivet editChatbotMessage endpoint) - editMessage: no longer calls response.json() on 204 No Content - deleteMessage: switch to DELETE /v2/im/chat/messages with robot_jid/account_id params - Tests: update MSG-03-a/b to assert chatbot IM API URL and structured content body - Tests: update MSG-04-a to assert correct im/chat endpoint - Tests: fix THRD-01 decodeThreadId test — channel-level ID now returns empty messageId
Per Zoom chatbot API spec (verified against @zoom/rivet source): - Store userJid from bot_notification and app_mention webhooks in threadUserJid map (keyed by threadId) - Include user_jid in postMessage body when available - Include user_jid in editMessage body when available - Include user_jid as query param in deleteMessage when available user_jid is the JID of the user who triggered the bot — required by the Zoom chatbot API for correct message attribution and notifications
- Rewrite Marketplace app setup with accurate step-by-step navigation aligned with actual Zoom Marketplace UI (General App, not Account-level) - Add Surface → Team Chat Subscription as correct location for Bot JID - Add Features → Access → Token as correct location for Secret Token - Add environment variables checklist with source locations - Add ngrok local testing instructions - Add Troubleshooting section covering common setup issues
|
Known gaps for future work (v2): The adapter currently handles
These require implementing Zoom's App Card format ( |
…r API) Zoom Team Chat does not support typing indicators. Replace NotImplementedError with a silent no-op so callers like onDirectMessage() don't need .catch() workarounds.
Zoom has no separate mention event — bot_notification fires for all bot interactions (DMs and slash commands). Setting isMention=true ensures onNewMention handlers fire consistently with other adapters.
|
@cramforce Great question! To clarify the scope: this adapter is Zoom Team Chat only — bots and slash commands don't work in the in-meeting chat UI, which is a current Zoom platform limitation. However, if users have Continuous Meeting Chat (CMC) enabled, the meeting chat content syncs into a persistent Team Chat channel. In that case the bot works naturally — users can interact with it in the CMC channel using the same slash commands, and the bot can see and respond to messages that originated in the meeting chat. So CMC bridges the gap nicely for teams that have it enabled. In short: in-meeting chat ❌, CMC channels (which surface meeting chat in Team Chat) ✅. I'll also double-check this internally with the Zoom chat team to confirm the CMC behavior and will update if anything is different. |
Consistent with bot_notification handling. team_chat.app_mention is an explicit @mention so isMention should always be true. In practice bot_notification covers all bot interactions but this handler is kept as a defensive fallback.
… @zoom/rivet) - thread.subscribe() does not work: Zoom bot_notification has no reply_to or parent_message_id field — confirmed against @zoom/rivet SDK which also has no thread reply handler - Update conversations feature table: thread subscription = No, typing = No - Clarify DM thread replies limitation
- replay-zoom tests now use onNewMention (not onNewMessage) to capture bot_notification and team_chat.app_mention events — SDK routes isMention messages exclusively to onNewMention handlers - Remove unused ALL_MESSAGES_PATTERN constant - Fix template literal lint error in markdown.ts (bullet list prefix)
Summary
@chat-adapter/zoomas a first-party platform adapter alongside Slack, Teams, and Google Chatapps/docs/adapters.jsonandskills/chat/SKILL.mdWhat's included
Webhook verification
endpoint.url_validation) with HMAC-SHA256 responsex-zm-signatureheader and raw body capture (timing-safe comparison)Auth
client_credentialsgrant, cached in-memory with 1-hour TTLInbound
bot_notificationandteam_chat.app_mentionevents into normalized SDKMessageobjectszoom:{channelId}:{messageId}formatOutbound
thread.post(),thread.edit(),thread.delete(), and threaded replies viareply_main_message_id/v2/im/chat/messages(verified against @zoom/rivet source)user_jidfrom incoming webhook context in all outbound requestsFormat conversion
ZoomFormatConverter: bidirectional Zoom markdown ↔ mdast (handles**bold**,_italic_,`code`,__underline__,~strikethrough~,# heading,* list)convertEmojiPlaceholdersstyleobject)Testing
bot_notification,team_chat.app_mention)Why
Zoom Team Chat has 8M+ business users. This completes the major enterprise messaging platforms alongside Slack, Teams, and Google Chat. Developers can build Zoom bots using the same Chat SDK API they already use for other platforms — no platform-specific code in the application layer.
I work at Zoom. This adapter is built and tested against real Zoom Marketplace app credentials. Happy to co-maintain.
Known limitations
chat_message.repliedfor 1:1 DMsWork in progress / follow-up
The following are known gaps being tracked for follow-up PRs:
onNewMentionnot triggered — Zoom has no separate mention event;bot_notificationcovers all interactions. Fix: setisMention=trueonbot_notificationmessagesrenderCard()not yet overridden; bold stripped during card-to-markdown conversionrenderCard()needs to produce Zoom's structured content body (buttons, fields, sections) instead of plain textinteractive_message_actions,interactive_message_select,interactive_message_editable,bot_installednot yet handledTest plan
pnpm validate— all 17 tasks pass (build, typecheck, lint, test, docs build)bot_notificationandteam_chat.app_mentionfixtures