> Кратко по-русски: это ПОЛНЫЙ сценарий проверки для команды обзора 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
| Setting | Production value |
| OAuth Redirect URL for OAuth | https://zvonok.zamlux.de/zoom/oauth/callback |
| OAuth allow list | zvonok.zamlux.de (single entry, no tunnels) |
| Deauthorization / event notification endpoint | https://zvonok.zamlux.de/zoom/webhook |
| Privacy policy | https://zvonok.zamlux.de/legal/privacy |
| Terms of use | https://zvonok.zamlux.de/legal/terms |
| Support | https://zvonok.zamlux.de/legal/support |
| Guide | https://zvonok.zamlux.de/legal/guide |
| This test plan | https://zvonok.zamlux.de/legal/reviewer-test-plan |
| Windows installer | https://zvonok.zamlux.de/download/Zvonok-Translator-Setup.exe |
Requested scopes — all four are used, none is decorative:
| Scope | Where it is used | Visible result for the reviewer |
meeting:write:meeting | СОЗДАТЬ ВСТРЕЧУ / ЗАПУСТИТЬ ВСТРЕЧУ (*create / start meeting*) on /admin/calls — creates a meeting on the consultant's own account | A new meeting number and join link appear in the app |
meeting:read:meeting | Before every join — decides "own meeting" vs "external meeting" | The app screen states which of the two paths it took |
user:read:token | Fetches a fresh On-Behalf-Of (OBF) token before joining a meeting hosted by another account | External-meeting join succeeds after approval |
user:read:zak | ZAK for start/join operations of the authorizing user | Join proceeds without a second sign-in prompt |
---
The build that the reviewer sees uses the Production credentials of this
Marketplace app, not the Development ones.
ZVONOK_ENV=production and pins the expected Production Client ID as ZOOM_EXPECTED_CLIENT_ID into the application
configuration (tools/build-windows-installer.js).
src/zoom/oauth.js compares the active 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.
---
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.
---
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.
---
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.
---
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.
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.
http://127.0.0.1:3000/zoom/oauth/start → *“Zoom authorization cannotstart”*: 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.
https://zvonok.zamlux.de/zoom/oauth/callback → *“Authorization was notcompleted”*: Zoom itself declined or the consent was cancelled. Press
Allow Zoom access again.
https://zvonok.zamlux.de/zoom/oauth/callback → *“Authorization could notbe completed”*: Zoom refused to issue the tokens; the short reason from Zoom is
printed on the page. Please send us that exact line.
http://127.0.0.1:3000/zoom/oauth/handoff → *“The application is notrunning on this computer”*: see 4a-1 — start the app and press the button
again.
http://127.0.0.1:3000/… (connection refused): the localserver 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.
---
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).
---
| # | Scope | Action in the app | Expected result |
| 6.1 | meeting:write:meeting | On /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.2 | meeting:read:meeting | On /admin/calls press ПРИСОЕДИНИТЬСЯ К ВСТРЕЧЕ (*join meeting*) with any meeting number | The 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.3 | user:read:token | Join a meeting hosted by Account B | The 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.4 | user:read:zak | Same join | ZAK 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.
---
> 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 screen | Meaning | What 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 it | this 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 list | starts 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.
---
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.
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.
---
app_deauthorized1. 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.
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.
---
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.
| Check | Expected |
Public pages /legal/privacy, /legal/terms, /legal/support, /legal/guide, /legal/reviewer-test-plan | 200, 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 install | never a 404 and never a credentials form — a redirect to the Zoom consent screen of the production app |
| Credentials asked from the installing user | none: no Zvonok login, no Client ID, no Client Secret |
http://127.0.0.1:3000/zoom/oauth/handoff | tokens arrive in the URL fragment (after #), so they never reach any server log |
/zoom/oauth/callback opened without a code | 200, explanatory page, nothing stored |
GET /zoom/webhook | 200, plain-text description |
POST /zoom/webhook with endpoint.url_validation | 200 with correct encryptedToken |
POST /zoom/webhook with a bad signature | 401, event not processed |
| Consent screen | Production client id, four scopes, redirect https://zvonok.zamlux.de/zoom/oauth/callback |
| Own-meeting join | Interpreter joins, video off, role 0, no recording |
| External-meeting join | OBF token per join; waiting room handled by waiting, not retrying |
| Deauthorization | 200 to Zoom, local tokens deleted, phase back to AUTH_REQUIRED |
| Interpreter joins a meeting | requires 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 joined | http://127.0.0.1:3000/admin/calls, buttons СОЗДАТЬ ВСТРЕЧУ / ПРИСОЕДИНИТЬСЯ К ВСТРЕЧЕ; the granted-access bar on /assistant links there |
| Temporary domains | none — production redirect, allow list and event endpoint all use the company-owned FQDN zvonok.zamlux.de |