Skip to content

feat(zoom): add Zoom API client and transcript parsing - #14481

Open
Subash-Mohan wants to merge 8 commits into
mainfrom
Subash-Mohan/zoom-client-foundation
Open

feat(zoom): add Zoom API client and transcript parsing#14481
Subash-Mohan wants to merge 8 commits into
mainfrom
Subash-Mohan/zoom-client-foundation

Conversation

@Subash-Mohan

@Subash-Mohan Subash-Mohan commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Description

  • Adds a Server-to-Server OAuth client that owns every call to Zoom, covering transcript fetch, occurrence details, and past-occurrence listing
  • Adds a WebVTT parser for the transcripts Zoom returns
  • No connector is registered yet, so indexing behaviour is unchanged

How Has This Been Tested?

Unit tests added for the client and the parser (32 tests).

Additional Options

  • [Optional] Please cherry-pick this PR to the latest release version.
  • [Optional] Override Linear Check

Summary by cubic

Adds a Zoom API client and WebVTT transcript parser as the foundation for the Zoom connector. No connector is registered yet, so indexing behavior is unchanged; the connector that uses this lands in a follow-up PR.

New Features

  • Handles Server-to-Server OAuth with early token refresh, retries once when Zoom rejects a cached token, and maps token-endpoint 401s to invalid-credential errors and 403s to insufficient-permission errors.
  • Double-encodes meeting UUIDs that start with "/" or contain "//" per Zoom's docs, and lists past occurrences because a recurring meeting's transcript endpoint only reaches the latest run.
  • Parses WebVTT positionally per the W3C spec so cue-like speech (a lone number or "--->") survives, and decodes character references so "R&D" is indexed as "R&D".
  • Refuses transcript download URLs outside zoom.us, sends the account-wide bearer token only to that host, and validates every redirect hop so an open redirect cannot reach a private address.
  • Treats 404 as "no recording" and only downloads when can_download, download_url, and download_restriction_reason agree.
  • Adds 32 unit tests covering the client and parser.

Written for commit 8f561d6. Summary will update on new commits.

Review in cubic

Groundwork for the Zoom connector: a Server-to-Server OAuth client that
owns every call to Zoom, and a WebVTT parser for the transcripts it
returns. Nothing is registered as a connector yet, so this changes no
indexing behaviour — the connector itself follows separately.

The client covers the endpoints the first connector needs: fetching a
Cloud Recording transcript, reading an occurrence's details, and listing
a meeting's past occurrences. A recurring meeting records each run
separately, and passing the bare meeting id to the transcript endpoint
only ever reaches the latest one, which is why the occurrence listing
exists.

Meeting UUIDs need care in a URL: they can contain "/", and Zoom's docs
require encoding twice when one starts with "/" or contains "//",
because something in front of their API decodes a layer first.

The VTT parser follows the W3C spec rather than matching on shape. Zoom
documents that the file is VTT and carries timestamped sections, but
never defines the layout inside it. Parsing by shape drops real speech:
a cue containing "-->" reads as a timing line, and a cue that is only a
number reads as a cue index. Both are covered by tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBNe54dXPELTXXGdk1uYR6
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Full-stack Preview (frontend + backend)

Status Preview Commit Updated
https://8f561d6-onyx.preview.onyxcorp.dev/ 8f561d6 2026-09-05 05:37:57 UTC

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🖼️ Visual Regression Report

Project Changed Added Removed Unchanged Report
admin 12 0 0 175 View Report
exclusive 0 0 0 10 ✅ No changes

@Subash-Mohan
Subash-Mohan marked this pull request as ready for review September 4, 2026 13:40
@Subash-Mohan
Subash-Mohan requested a review from a team as a code owner September 4, 2026 13:40
@greptile-apps

greptile-apps Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds the foundation for a Zoom connector without registering an indexing connector.

  • Adds a Server-to-Server OAuth client with token caching, refresh, retry, and typed authorization errors.
  • Adds guarded transcript downloads with Zoom-host validation and redirect SSRF protection.
  • Adds models for transcripts, meeting details, and recurring meeting occurrences.
  • Adds a WebVTT parser that preserves speech while removing cue metadata and decoding character references.
  • Adds unit coverage for authentication, URL handling, redirects, API models, and transcript parsing.

Confidence Score: 5/5

The PR appears safe to merge; no outstanding or newly introduced actionable failures remain.

The latest revision correctly classifies token endpoint authorization failures with connector-validation exceptions. All previous findings were manually resolved, and the current code contains their corresponding fixes.

Important Files Changed

Filename Overview
backend/onyx/connectors/zoom/client.py Implements Zoom authentication, meeting APIs, typed errors, safe transcript downloads, and stale-token retry.
backend/onyx/connectors/zoom/models.py Defines typed Zoom response models and conservative transcript readiness checks.
backend/onyx/connectors/zoom/recordings/vtt.py Parses WebVTT cues into normalized searchable transcript text.
backend/tests/unit/onyx/connectors/zoom/test_zoom_client.py Covers client authentication, API behavior, URL validation, redirects, retries, and error mapping.
backend/tests/unit/onyx/connectors/zoom/test_zoom_vtt.py Covers transcript parsing, reserved blocks, cue-like speech, markup, and character references.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[ZoomClient] --> B[Zoom OAuth token endpoint]
    A --> C[Zoom meetings API]
    C --> D[Transcript metadata]
    D --> E{Downloadable?}
    E -->|Yes| F[Validated Zoom download URL]
    F --> G[SSRF-safe redirect handling]
    G --> H[WebVTT parser]
    H --> I[Indexable transcript text]
    E -->|No| J[Skip transcript]
Loading

Reviews (4): Last reviewed commit: "test(zoom): run the real guard in the do..." | Re-trigger Greptile

Comment thread backend/onyx/connectors/zoom/client.py
Comment thread backend/onyx/connectors/zoom/client.py Outdated
Comment thread backend/onyx/connectors/zoom/recordings/vtt.py

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread backend/onyx/connectors/zoom/client.py
Comment thread backend/onyx/connectors/zoom/client.py Outdated
Comment thread backend/onyx/connectors/zoom/client.py Outdated
Comment thread backend/onyx/connectors/zoom/client.py Outdated
Comment thread backend/onyx/connectors/zoom/recordings/vtt.py Outdated
Comment thread backend/onyx/connectors/zoom/recordings/vtt.py Outdated
Comment thread backend/onyx/connectors/zoom/client.py Outdated
Comment thread backend/onyx/connectors/zoom/client.py
Subash-Mohan and others added 4 commits September 5, 2026 08:48
Zoom can reject an access token before the expiry it handed us, so a 401
on a normal request did not mean the credentials were bad. Reporting it
as CredentialExpiredError cancelled the whole indexing attempt, and five
of those in a row mark the connector invalid and email the admins — over
credentials that were fine. A 401 now clears the cached token and retries
once, and only a second rejection is reported.

The transcript download went straight to requests.get, so it had no
retry and surfaced a bare HTTPError instead of the typed errors every
other call maps. Both paths now share one helper, so they cannot drift
apart again.

Retrying is safe here because a 401 is refused before the request does
anything, so there is no side effect to repeat.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBNe54dXPELTXXGdk1uYR6
download_transcript_vtt took any URL and attached the account-wide bearer
token to it, so a wrong or tampered download URL would hand the credential
to whatever host it named, or reach an internal service. Zoom supplies the
URL, but that is not a reason to send a credential somewhere unchecked.

The URL is now rejected unless it is a zoom.us host, and it also goes
through the repository's outbound URL check so a Zoom subdomain pointing
at an internal address is refused too. Only the first host needs checking,
because requests drops the Authorization header on a cross-host redirect —
which matters, since Zoom's downloads legitimately redirect to a CDN.

The allowlist matches the API base URL this client already hardcodes, so
it adds no limitation. A test ties the two together: Zoom for Government
lives on api.zoomgov.com, and pointing the client there without moving the
allowlist would fail every download.

A rejected URL raises ValueError rather than a validation error, so one
bad URL fails its own document instead of cancelling the indexing run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBNe54dXPELTXXGdk1uYR6
WebVTT forbids a literal "&" or "<" in cue text, so a speaker saying
"R&D" reaches us as "R&amp;D". The parser stripped markup but never
decoded, so that went into the index verbatim and nobody searching for
"R&D" would find the meeting.

Decoding happens after markup is stripped, not before. An escaped
"&lt;v Jane&gt;" is something a speaker actually said; decoding first
would turn it into a tag and delete it.

A decoded non-breaking space becomes a normal one, matching what the
Fireflies connector already does, because it is invisible but will not
match a space someone types into search.

Also moves the module's two shape-matching gotchas into a header rather
than repeating them beside each regex.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBNe54dXPELTXXGdk1uYR6
Transcript downloads bypassed the session and ran with no retries, so one
transient 429 lost a transcript that every other call would have retried.
Mount the retry adapter on the scheme, not per URL: the API, the token
endpoint and the download are three different Zoom hosts, and a host
without its own mount falls back to the no-retry default.

Readiness came from download_url alone. Zoom documents can_download,
download_url and download_restriction_reason as mutually exclusive, but
its own example returns all three together. Require all three to agree.

Keep meeting_topic, host_id and can_download from the transcript
response. meeting_topic is the only field the connector needs from
GET /past_meetings/{id}, which is the one call that Zoom limits to
meetings less than one year old.

Report Zoom's error code and message. The requests library stops at
"400 Client Error" and drops the body, which hides codes such as 12702.

Remove the next_page_token check. The endpoint defines three fields and
no paging token, and the truncation that does occur is Zoom's 15-month
limit, which no check can detect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBNe54dXPELTXXGdk1uYR6
@Subash-Mohan

Copy link
Copy Markdown
Contributor Author

@greptile @cubic

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

@greptile @cubic

@Subash-Mohan I have started the AI code review. It will take a few minutes to complete.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Go coverage

tools/ods

Packages: 1 improved. Total coverage is 36.5% (+0.1 against the baseline).

Package Coverage Floor Change
internal/portutil 91.7% 75.0% +16.7
total 36.5% 36.4% +0.1

Browse the uncovered lines

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread backend/onyx/connectors/zoom/client.py
The download URL was checked once, then requests followed the redirects it
returned without any further check. An open redirect on a Zoom host made
the connector fetch a private address and index the body as a transcript.
Dropping the Authorization header on a cross-host redirect stops the token
leaking, but it does not stop the request.

Stop following redirects automatically and send each hop through
ssrf_safe_get, which resolves and validates the address and pins it
against DNS rebinding.

Do not pass the token to ssrf_safe_get: it forwards the same headers to
every hop, so the account-wide credential would reach each redirect
target. The first request keeps the session, and with it the retry policy,
because ssrf_safe_get makes a new session per hop and does not retry. The
storage host authenticates from the signed URL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBNe54dXPELTXXGdk1uYR6
@Subash-Mohan

Copy link
Copy Markdown
Contributor Author

@greptile @cubic

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

@greptile @cubic

@Subash-Mohan I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread backend/onyx/connectors/zoom/client.py
Comment thread backend/tests/unit/onyx/connectors/zoom/test_zoom_client.py Outdated
Subash-Mohan and others added 2 commits September 5, 2026 11:00
A 403 from Zoom's token endpoint raised a plain HTTPError, so a permanent
misconfiguration such as a deactivated app or a scope that was never
granted looked transient. Onyx retried it forever and never told the
admin. Map it to InsufficientPermissionsError, which is what the API path
already does.

Raise CredentialInvalidError instead of CredentialExpiredError on a 401.
A Server-to-Server client secret has no expiry, so "expired" sends admins
to look for a renewal that does not exist. Both types are terminal, so
only the message an admin reads changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBNe54dXPELTXXGdk1uYR6
The test re-implemented the host check in its assertion instead of
calling it. It caught a change to _API_BASE_URL, but gutting
_reject_non_zoom_download_url left it passing, because the assertion
verified the two constants agreed under the test's own copy of the
matching rule rather than the guard's.

Call the guard with a URL built from the API host, so both sides run and
neither is duplicated. The test now fails when _API_BASE_URL moves away
from the allowlist and when the guard's own predicate changes. _ZOOM_HOST
is no longer imported, since the test no longer repeats the rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBNe54dXPELTXXGdk1uYR6
@Subash-Mohan

Copy link
Copy Markdown
Contributor Author

@greptile @cubic

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

@greptile @cubic

@Subash-Mohan I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 7 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Re-trigger cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant