Zoom Reviewer Test Plan — Zvonok Translator

> Кратко по-русски: это ПОЛНЫЙ сценарий проверки для команды обзора Zoom App

> Marketplace. Он на английском намеренно. Короткая обзорная записка лежит

> рядом: docs/zoom-reviewer-instructions.md.

Publisher: Zamluxury GmbH

App name: Zvonok Translator

App type: General app, user-managed, Meeting SDK embed + programmatic join

Distribution: unlisted (single tax consultancy, not offered publicly)

Public domain (company-owned, permanent): zvonok.zamlux.de

SettingProduction value
OAuth Redirect URL for OAuthhttps://zvonok.zamlux.de/zoom/oauth/callback
OAuth allow listzvonok.zamlux.de (single entry, no tunnels)
Deauthorization / event notification endpointhttps://zvonok.zamlux.de/zoom/webhook
Privacy policyhttps://zvonok.zamlux.de/legal/privacy
Terms of usehttps://zvonok.zamlux.de/legal/terms
Supporthttps://zvonok.zamlux.de/legal/support
Guidehttps://zvonok.zamlux.de/legal/guide
This test planhttps://zvonok.zamlux.de/legal/reviewer-test-plan
Windows installerhttps://zvonok.zamlux.de/download/Zvonok-Translator-Setup.exe

Requested scopes — all four are used, none is decorative:

ScopeWhere it is usedVisible result for the reviewer
meeting:write:meetingСОЗДАТЬ ВСТРЕЧУ / ЗАПУСТИТЬ ВСТРЕЧУ (*create / start meeting*) on /admin/calls — creates a meeting on the consultant's own accountA new meeting number and join link appear in the app
meeting:read:meetingBefore every join — decides "own meeting" vs "external meeting"The app screen states which of the two paths it took
user:read:tokenFetches a fresh On-Behalf-Of (OBF) token before joining a meeting hosted by another accountExternal-meeting join succeeds after approval
user:read:zakZAK for start/join operations of the authorizing userJoin proceeds without a second sign-in prompt

---

0. Statement about the Production Client ID

The build that the reviewer sees uses the Production credentials of this

Marketplace app, not the Development ones.

Production Client ID as ZOOM_EXPECTED_CLIENT_ID into the application

configuration (tools/build-windows-installer.js).

ZOOM_CLIENT_ID with the pinned Production Client ID and **refuses to start

authorization** if they differ, if the redirect URL is not https, or if the

redirect URL points at localhost, 127.0.0.1 or any tunnel host such as

ngrok. The failure is shown on screen as ZOOM_PRODUCTION_MISMATCH.

The consent screen the reviewer sees is the Production app.

Verification for the reviewer: on the consent screen, the client id in the

address bar (https://zoom.us/oauth/authorize?...&client_id=…) is the

Production Client ID of this submission, and redirect_uri is exactly

https://zvonok.zamlux.de/zoom/oauth/callback.

---

1. Prerequisites

About the environment. Zvonok Translator is not a website. It is a Windows

desktop application: a local Node.js server plus the native **Zoom Meeting SDK

for Windows 7.1.5** running in a separate helper process, installed on the

consultant's own computer. It has no multi-tenant backend, no user database and

no cloud login. The only public surface is the static site above (documents,

the OAuth return page and the Zoom event endpoint).

Required for a full test:

1. Windows 11 x64, no administrator rights needed for the installer.

2. Two Zoom accounts:

* Account A — the consultant account that authorizes the app;

* Account B — any second account used to host an *external* meeting

(this is the reviewer's own account in scenario B).

3. Zoom Workplace desktop client signed in as Account A on the test machine.

4. Zoom plan: a free Zoom account is sufficient for scenarios A and B.

Nothing in the app requires a paid plan, Zoom Phone, webinars or cloud

recording. If the reviewer wants to test a meeting longer than the free

40-minute limit, a licensed account is convenient but not required.

5. 2FA: if the consultant account has two-factor authentication enabled,

Zoom asks for the second factor once on the consent screen. The app itself

never handles passwords or 2FA codes — it only receives the OAuth

authorization code on the redirect URL.

6. Meeting SDK runtime: the public installer contains the application, the

Node.js runtime and the JavaScript side of the Meeting SDK bridge. The

native Zoom Meeting SDK binaries (~315 MB) are not part of the public

download; they are delivered with the on-premise package for the consultancy

(the same build script produces it with ZVONOK_INCLUDE_MEETING_SDK=1).

Consequence for the review: steps that only need OAuth, scopes, webhook and

deauthorization can be executed by the reviewer from the public installer

alone; steps that require the interpreter to actually enter a meeting are

demonstrated live (section 2, option B). **This is the single reason an

interpreter never appears in a meeting started from the public download —

see section 7a, which also names the page and the buttons.**

Seed data. None is required. The app ships with an empty client database

(src/clients.json is written empty by the installer), empty data/ and empty

logs/. No demo tenant, no fixtures, no sample users have to be created. If

the reviewer wants a populated consultant screen, any spoken sentence during

the call fills the transcript and hint panels within a second.

---

2. Account credentials / how the reviewer gets access

There is no separate Zvonok login, and we do not want to invent one. The

application has no accounts of its own: it authenticates the consultant against

Zoom and nothing else. There is no admin console, no tenant, no password to

hand over. Anything shaped like "username / password for your app" would be a

fake.

Two honest ways to exercise the app:

**No credentials of any kind are requested from the reviewer — not a Zvonok

login, not a Client ID, not a Client Secret.** The reviewer authorizes with his

*own* Zoom account through the normal OAuth flow of the production app. See

section 4a, *Zoom authorization — start here*.

https://zvonok.zamlux.de/download/Zvonok-Translator-Setup.exe, install,

press Allow Zoom access and approve the Zoom consent screen with the

reviewer's own account (section 4a). Everything

in sections 3, 4, 5, 6, 9 and 10 (OAuth, scope validation, deauthorization,

token deletion) works with the public installer. To also run the in-meeting part on the

reviewer's machine, request the package that includes the native Meeting SDK

runtime through the support address on the listing page; we send a direct

download link for the same signed installer built with the SDK included.

hosts a Zoom meeting, we join with the consultant machine and share the

screen. Every step in sections 5–7 is shown live, including the app's own

diagnostic panel (join path, OBF usage, raw-audio counters, waiting-room

state). Please use the support address on the listing page to arrange a slot;

we accommodate the reviewer's time zone.

---

3. Installation

1. Open https://zvonok.zamlux.de/ and press **Download for Windows 11

(64-bit)**.

2. Optional integrity check: the SHA-256 of the file is published next to it at

/download/Zvonok-Translator-Setup.exe.sha256.

3. Run Zvonok-Translator-Setup.exe. It installs per user (no administrator

prompt) and creates:

* a Desktop shortcut named *Zvonok Translator*;

* a Start menu entry with the same name.

4. Expected: the installer finishes without a UAC prompt and without asking for

Node.js — its own node.exe copy is inside the package.

---

4. Launch

1. Double-click the Zvonok Translator shortcut.

2. Expected: the launcher starts the local server and opens the consultant

screen at http://127.0.0.1:3000/assistant. The server binds to the

loopback interface only; it is not reachable from the network.

3. Expected on first start: the Zoom panel shows phase

AUTH_REQUIRED together with the redirect URL

https://zvonok.zamlux.de/zoom/oauth/callback.

4. Negative check: if the build were mis-configured (development client id,

http redirect, tunnel host), this panel would instead show

ZOOM_PRODUCTION_MISMATCH and the Allow Zoom access button would refuse

to open a consent screen at all.

4a. ZOOM AUTHORIZATION — START HERE

One button. Nothing to type in. No Client ID, no Client Secret.

> Fixed on 16.09.2026 — please download the installer again before testing.

> The build published until 15.09.2026 shipped a configuration file in which the

> production Client ID was empty, because the build machine did not provide it.

> On a fresh installation that single empty value made the app answer the

> Allow Zoom access button with the error page *“Zoom authorization cannot

> start”* instead of opening the Zoom consent screen — this is the error shown

> in the reviewer's walkthrough. The Client ID of the published app is now

> fixed in the program code itself, so it can no longer be missing, and the

> build refuses to produce an installer without it.

> Current download: <https://zvonok.zamlux.de/download/Zvonok-Translator-Setup.exe>

> (SHA-256 published next to it at …/Zvonok-Translator-Setup.exe.sha256).

> If an older copy is already installed, run the new installer over it — it

> replaces the configuration file.

1. On the consultant screen (http://127.0.0.1:3000/assistant, also

http://127.0.0.1:3000/) a blue button **“Allow Zoom access · Разрешить

доступ Zoom”** is shown at the top for as long as access has not been

granted. Press it.

*Equivalent:* open http://127.0.0.1:3000/zoom/oauth/start on the same

machine — the button leads exactly there.

2. The default browser opens the Zoom consent screen of the production

Marketplace app. Sign in with the reviewer's own Zoom account and press

Allow.

3. Done. The button disappears and the Zoom panel shows phase AUTHORIZED

together with the granted scopes. Continue with section 6.

Why the reviewer is never asked for keys (fixed on 05.09.2026). The Client

Secret belongs to the integration, not to the person using it, so it lives in

the environment of our own backend on zvonok.zamlux.de and nowhere else. The

authorization code is exchanged for tokens there, by us, and only the

resulting user tokens are handed to the application on the reviewer's computer —

in the URL *fragment*, which browsers never send to any server. Consequently the

public installer needs no secret, the reviewer enters nothing, and installing

plus authorizing this app is a plain OAuth flow driven by the production

Client ID.

> http://127.0.0.1:3000/zoom/setup still exists, but it is not part of

> this flow and the app never sends anyone there. It serves a different case

> only: an organisation deploying this software against *its own* Zoom

> Marketplace app instead of ours.

4a-1. Keep Zvonok Translator running. The consent screen is opened in the

browser, but the granted access is handed back to the application on the same

computer. If the app window is closed (or the launcher stopped) while the

browser is on the Zoom consent screen, the return page cannot reach it. Leave

the app running for the whole of section 4a.

4a-2. If an error page appears — what it means. Every stage shows a page

with its own address, so the address alone identifies the stage. Verified live

on 12.09.2026, rechecked on 16.09.2026.

start”*: the installation itself is mis-configured. This is exactly what the

build published until 15.09.2026 did on a fresh machine, and it is fixed in the

current download. Re-run the installer from

https://zvonok.zamlux.de/download/Zvonok-Translator-Setup.exe; nothing has to

be typed in.

completed”*: Zoom itself declined or the consent was cancelled. Press

Allow Zoom access again.

be completed”*: Zoom refused to issue the tokens; the short reason from Zoom is

printed on the page. Please send us that exact line.

running on this computer”*: see 4a-1 — start the app and press the button

again.

server is not running; start Zvonok Translator from the Desktop shortcut.

The backend state can be checked at any time, from any machine and without

installing anything, at https://zvonok.zamlux.de/zoom/oauth/token — it answers

configured: true, productionKeys: true and the redirect URL. No key, secret

or token is ever shown there.

---

5. Authorization (OAuth) — step by step

1. Press Allow Zoom access in the app, or open

http://127.0.0.1:3000/zoom/oauth/start directly on the same machine (both

lead to the same place; the address is served whenever the app is running,

and it never asks for credentials — it redirects straight to Zoom).

2. The default browser opens

`https://zoom.us/oauth/authorize?response_type=code&client_id=<Production

Client ID>&redirect_uri=https%3A%2F%2Fzvonok.zamlux.de%2Fzoom%2Foauth%2Fcallback&state=<random>`.

3. Sign in as the test account (plus 2FA if enabled) and review the consent

screen. Expected: exactly four scopes are requested —

meeting:write:meeting, meeting:read:meeting, user:read:token,

user:read:zak. No scope beyond these is requested.

4. Press Allow.

5. Zoom returns to https://zvonok.zamlux.de/zoom/oauth/callback?code=…&state=….

Expected: a page titled *"Authorization received"*. This endpoint exchanges

the authorization code for the user's tokens with the production Client ID

and Client Secret held in its own environment, and then hands the result to

the application on 127.0.0.1 in the URL fragment

(http://127.0.0.1:3000/zoom/oauth/handoff#…). A fragment is never

transmitted to any server, so no token appears in a request line, an access

log or a Referer header. Nothing is stored or logged in the cloud: the

function keeps no database and writes no request bodies.

6. Expected in the app: phase becomes AUTHORIZED, the granted scope list

is displayed, missingScopes is empty. No token value is ever printed on

screen or in the logs.

7. Negative check — replayed state: opening the same callback URL a second

time is rejected (state is single-use, 15-minute lifetime).

---

6. Scope-by-scope validation

#ScopeAction in the appExpected result
6.1meeting:write:meetingOn /admin/calls press СОЗДАТЬ ВСТРЕЧУ → СОЗДАТЬ СЕЙЧАС (*create meeting* → *create now*)POST /users/me/meetings succeeds; the app shows the new meeting number and join URL. Without the scope the button is disabled and states which scope is missing
6.2meeting:read:meetingOn /admin/calls press ПРИСОЕДИНИТЬСЯ К ВСТРЕЧЕ (*join meeting*) with any meeting numberThe app first calls GET /meetings/{id} to decide own-vs-external and prints the decision ("own meeting — SDK signature only" / "external meeting — OBF token required")
6.3user:read:tokenJoin a meeting hosted by Account BThe app calls GET /users/me/token?type=onbehalf per join, uses the token immediately, never caches and never logs it. The diagnostic panel shows "OBF token: fresh, used once"
6.4user:read:zakSame joinZAK is fetched for the authorizing user and passed to the Meeting SDK; no additional sign-in prompt appears

If a scope is revoked in Marketplace, the app reports it in missingScopes and

blocks exactly the corresponding action instead of failing at runtime.

---

7a. HOW THE INTERPRETER JOINS A MEETING — START HERE

> Added on 23.09.2026 after the reviewer's question of 21.09.2026

> ("I was able to authorize the app and create a meeting but I was unable to

> get it to join the meeting").

There is no cloud bot. Nothing of ours dials into a meeting from the

internet, and nothing joins a meeting number on its own. The *interpreter* is a

participant opened by the native Zoom Meeting SDK for Windows 7.1.5 running

as a helper process of Zvonok Translator on the consultant's own computer.

It therefore appears only while that computer runs the app and only for the

meeting the consultant is currently in. Creating a meeting does not make it

appear — a join has to be started in the app.

Why it did not join for the reviewer. The public download

(Zvonok-Translator-Setup.exe, ~42 MB) deliberately contains **no native

Meeting SDK binaries** (~315 MB; see section 1, item 6). Everything that OAuth,

scopes, the webhook and deauthorization need is in it — the join step is not.

Until 23.09.2026 the app reported that in Russian developer wording

(ZOOM_SDK_BIN не задан — соберите zoom-sidecar/native), which gave the

reviewer no way to understand what was missing. Fixed on 23.09.2026: the app

now answers the join button with a plain sentence —

> *NATIVE MEETING SDK — Meeting SDK runtime is not part of this build, so the

> interpreter cannot join a meeting from it. Request the full package (support

> address on the app listing) or ask for a live demonstration. Details:

> https://zvonok.zamlux.de/legal/reviewer-test-plan (section 7a).*

So: an audio source, the meeting language or the 40-minute limit were never the

problem, and no setting inside the app would have helped. Playing a German

video was not necessary.

Where the join controls are — this is the other half of the answer. The

launcher opens the *souffleur* screen http://127.0.0.1:3000/assistant, and

that screen has no meeting controls at all: after the consent screen the

"Allow Zoom access" bar simply disappeared there and pointed nowhere. Meetings

live on a different page:

> ### http://127.0.0.1:3000/admin/calls — «ЦЕНТР ЗВОНКОВ» (call centre)

Fixed on 23.09.2026: once Zoom access is granted, the bar on /assistant and

/ no longer vanishes — it now reads *"Zoom access granted. Meetings are

created and joined in the call centre"* and carries a green button

Open call centre · Центр звонков leading straight to /admin/calls.

The buttons, exactly as they are labelled on screen. The call centre is in

Russian, because the consultancy that uses it is Russian-speaking:

Button on screenMeaningWhat it does
ZOOM*Zoom mode* — top of the page, next to ОБЫЧНЫЙ ЗВОНОК (*phone call*)switches the page to meetings; press it first
СОЗДАТЬ ВСТРЕЧУ → СОЗДАТЬ СЕЙЧАС*Create meeting* → *create now*creates a meeting on your own account (meeting:write:meeting)
ВОЙТИ ВО ВСТРЕЧУ*Enter the meeting*opens the meeting you just created in Zoom Workplace
ПРИСОЕДИНИТЬСЯ К ВСТРЕЧЕ*Join meeting* — paste any meeting link or ID above itthis is the button that sends the interpreter into the meeting
ПОДГОТОВИТЬ*Prepare*runs the readiness check only, joins nothing
ЗАПУСТИТЬ ВСТРЕЧУ*Start meeting* — on a card in the meeting liststarts your own meeting as host and sends the interpreter in
ЗАВЕРШИТЬ*Stop*the interpreter leaves; the meeting continues for the humans

ПОДГОТОВИТЬ is the fastest way to reproduce the finding: on the public build

it reports NATIVE MEETING SDK with the sentence quoted above, and it does so

without starting anything.

Two honest ways to see the interpreter actually join.

1. Full package. The same signed installer, built with the Meeting SDK

runtime included (~360 MB). It is not published on the website because the

static site is not a place for a 360 MB file. Write to the support address

on the app listing page (or reply in this review thread) and we send a

direct download link straight away; nothing has to be typed into the app

after installing it — the OAuth already granted stays valid.

2. Live demonstration (unique testing environment). The reviewer hosts a

meeting, we join from the consultant machine and share the screen: the

interpreter appearing in the participant list, video off, role = 0, the

German↔Russian audio, the diagnostic panel and Stop. We work around the

reviewer's time zone.

Expected result once the runtime is present: pressing ЗАПУСТИТЬ ВСТРЕЧУ

(own meeting) or ПРИСОЕДИНИТЬСЯ К ВСТРЕЧЕ (someone else's meeting) makes a

participant named as a translator appear in the meeting within a few seconds,

with its camera off and without any recording indicator. Sections 7 and 8

below describe both cases in full.

---

7. Meeting flow — scenario A (meeting hosted by the consultant's own account)

1. In the app press ЗАПУСТИТЬ ВСТРЕЧУ (*Start meeting*, uses

meeting:write:meeting). Requires the Meeting SDK runtime — see section 7a.

2. Join that meeting from Zoom Workplace as Account A, camera and microphone on.

3. The app joins the same meeting as an extra participant, the *interpreter*.

Expected:

* the interpreter is visible in the participant list with a name marking it

as a translator;

* the interpreter's video is off — the app never turns on a camera;

* the interpreter joins with role = 0 (participant), never as host;

* no recording indicator appears — the app never starts cloud or local

recording.

4. Speak German on the remote side. Expected: within ~1–2 s the consultant

screen shows the Russian text and the Russian speech is played into the

meeting.

5. Speak Russian as the consultant. Expected: the remote side hears

German synthesized speech. The raw Russian audio is *not* forwarded — the

app displays the counter "raw Russian frames sent = 0".

6. Press ЗАВЕРШИТЬ (*stop*) in the app. Expected: the interpreter leaves the meeting, the

meeting itself continues for the humans.

8. Meeting flow — scenario B (external meeting, multi-user)

1. The reviewer (Account B) hosts a meeting and sends the link to Account A.

2. Account A joins in Zoom Workplace.

3. On /admin/calls press ПРИСОЕДИНИТЬСЯ К ВСТРЕЧЕ (*join meeting*). Expected: the app detects the

meeting is *not* on Account A's account, fetches a fresh OBF token

(user:read:token) and joins with role = 0.

4. Waiting room: if the host has the waiting room enabled, the app shows

*"waiting for admission"* and simply waits. When the host admits the

interpreter, the app continues on its own — no restart, no second join

attempt, no retry storm. If the host never admits it, the app stops after

the configured wait and says so; it does not hammer Zoom.

5. Not-in-meeting case: if Account A is not yet in the meeting, Zoom answers

MEETING_FAIL_AUTHORIZED_USER_NOT_INMEETING (501). Expected: the app treats

this as "wait for the person" and retries with backoff, not as a failure.

6. Before approval: until this submission is approved, Zoom answers this

scenario with MEETING_FAIL_UNABLE_TO_JOIN_EXTERNAL_MEETING (63). Expected:

the app reports that honestly on screen and does not retry, because

retrying cannot change the answer. After approval the same step succeeds

unchanged — no code path is switched on for the review only.

7. Multi-user: with three or more participants, each speaker's language is

detected per utterance; the interpreter still sends only the translated

audio and still shows only one hint stream to the consultant.

---

9. Deauthorization and app_deauthorized

1. Endpoint check without Zoom: GET https://zvonok.zamlux.de/zoom/webhook

answers 200 with a one-line explanation (it exists, it is not a 404, and

it is not behind a tunnel).

2. URL validation: Zoom's Validate button in the Event Subscription screen

sends POST with event = endpoint.url_validation. The endpoint answers

200 with `{"plainToken": …, "encryptedToken": HMAC-SHA256(plainToken,

secret token)}`, exactly as required.

3. Signature verification: any other event must carry a valid

x-zm-signature computed over v0:<x-zm-request-timestamp>:<raw body>.

An event with a wrong or missing signature is rejected with 401 and is

not processed.

4. Remove the app: Zoom → Settings → Apps → Zvonok Translator → Remove.

5. Expected: Zoom delivers app_deauthorized to

https://zvonok.zamlux.de/zoom/webhook, which acknowledges with 200.

10. Data deletion / token cleanup

is no database, no object storage and no cloud copy of any Zoom token,

meeting, transcript or recording. The webhook function writes no request

bodies to the logs.

the deauthorized user immediately — the token file is removed

(deauthorize() in src/zoom/oauth.js), the in-memory copy is dropped and

the phase returns to AUTH_REQUIRED. After that the app can no longer obtain

an OBF token and cannot join any meeting on that user's behalf.

Disconnect Zoom, without going through Marketplace.

user profile. The app removes inherited permissions and grants access to the

consultant's Windows account only (icacls /inheritance:r /grant:r <user>:F).

Local administrators and SYSTEM keep the access Windows always gives them.

This is stated identically in the privacy policy — the wording matches the

code, not a Unix file mode.

are deleted according to the consultancy's own retention rules.

---

11. What the app deliberately does not do

the meeting number the consultant is currently in.

a purpose other than the one listed in section 6.

speech-synthesis providers named in the privacy policy.

line, a URL it prints, or a page it renders.

12. Expected results — summary checklist

CheckExpected
Public pages /legal/privacy, /legal/terms, /legal/support, /legal/guide, /legal/reviewer-test-plan200, readable without any login
Allow Zoom access button on /assistant and /visible while access is not granted; leads to the Zoom consent screen
http://127.0.0.1:3000/zoom/oauth/start on a fresh installnever a 404 and never a credentials form — a redirect to the Zoom consent screen of the production app
Credentials asked from the installing usernone: no Zvonok login, no Client ID, no Client Secret
http://127.0.0.1:3000/zoom/oauth/handofftokens arrive in the URL fragment (after #), so they never reach any server log
/zoom/oauth/callback opened without a code200, explanatory page, nothing stored
GET /zoom/webhook200, plain-text description
POST /zoom/webhook with endpoint.url_validation200 with correct encryptedToken
POST /zoom/webhook with a bad signature401, event not processed
Consent screenProduction client id, four scopes, redirect https://zvonok.zamlux.de/zoom/oauth/callback
Own-meeting joinInterpreter joins, video off, role 0, no recording
External-meeting joinOBF token per join; waiting room handled by waiting, not retrying
Deauthorization200 to Zoom, local tokens deleted, phase back to AUTH_REQUIRED
Interpreter joins a meetingrequires the Meeting SDK runtime; from the public download the app answers NATIVE MEETING SDK — Meeting SDK runtime is not part of this build… (section 7a)
Where meetings are created and joinedhttp://127.0.0.1:3000/admin/calls, buttons СОЗДАТЬ ВСТРЕЧУ / ПРИСОЕДИНИТЬСЯ К ВСТРЕЧЕ; the granted-access bar on /assistant links there
Temporary domainsnone — production redirect, allow list and event endpoint all use the company-owned FQDN zvonok.zamlux.de