From 14b06993a822426e91e8c807769bfccd8dabf00b Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sun, 20 Sep 2026 08:38:16 +0200 Subject: [PATCH 1/5] docs(calls): document number_restricted and call_restricted in the calls commands --- README.md | 7 +++++++ src/commands/calls/invite.ts | 2 +- src/commands/calls/place.ts | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3d206bde..525095aa 100644 --- a/README.md +++ b/README.md @@ -486,6 +486,13 @@ never answers or declines on its own. the answered call. `calls reject` exits with code 4 and `code: "call_not_ringing"` when the call was already answered, declined, or ended. Declining ends the call for every participant. +- **Number restrictions.** When WhatsApp restricts the Number to people it has + already chatted with, `calls place` and `calls invite` exit with code 3 and + `code: "number_restricted"` for anyone else; calls to existing contacts still + work. A call WhatsApp refuses for that reason ends with + `reason: "call_restricted"` in `calls watch`. `polymorfa listen + --events session.restriction_updated` reports the restriction starting and + ending. - **Connections.** `calls leave` closes one connection, named by an 8 to 64 character ID from `A-Z`, `a-z`, `0-9`, `_`, and `-`. The call continues. `calls end` ends the call for everyone. diff --git a/src/commands/calls/invite.ts b/src/commands/calls/invite.ts index b47ca60a..7892ff1a 100644 --- a/src/commands/calls/invite.ts +++ b/src/commands/calls/invite.ts @@ -14,7 +14,7 @@ export default class CallsInvite extends BaseCommand { } static override description = - 'Ring another WhatsApp user into a call, turning a 1:1 call into a group call. The call must have a live leg.' + 'Ring another WhatsApp user into a call, turning a 1:1 call into a group call. The call must have a live leg. While WhatsApp restricts the number to people it has already chatted with, inviting anyone else exits with code 3 and number_restricted.' static override examples = [ '<%= config.bin %> calls invite call_123 +15550101', diff --git a/src/commands/calls/place.ts b/src/commands/calls/place.ts index b7d8066a..a0468ba7 100644 --- a/src/commands/calls/place.ts +++ b/src/commands/calls/place.ts @@ -13,7 +13,7 @@ export default class CallsPlace extends BaseCommand { } static override description = - 'Place a WhatsApp call from a session. The placing participant owns the call. Server API keys name the session with --session; client tokens place from the session they are bound to.' + 'Place a WhatsApp call from a session. The placing participant owns the call. Server API keys name the session with --session; client tokens place from the session they are bound to. While WhatsApp restricts the number to people it has already chatted with, a call to anyone else exits with code 3 and number_restricted.' static override examples = [ '<%= config.bin %> calls place +15550100 --session support', From ed5130ea45c10d6c9eb93bedc33bdf26fd58ac05 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sun, 20 Sep 2026 16:12:17 +0200 Subject: [PATCH 2/5] style: apply Prettier to the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 525095aa..77f442e1 100644 --- a/README.md +++ b/README.md @@ -491,7 +491,7 @@ never answers or declines on its own. `code: "number_restricted"` for anyone else; calls to existing contacts still work. A call WhatsApp refuses for that reason ends with `reason: "call_restricted"` in `calls watch`. `polymorfa listen - --events session.restriction_updated` reports the restriction starting and +--events session.restriction_updated` reports the restriction starting and ending. - **Connections.** `calls leave` closes one connection, named by an 8 to 64 character ID from `A-Z`, `a-z`, `0-9`, `_`, and `-`. The call continues. From 7bf1aaede5f87688d7c0699fab7b1d56ffdbc723 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Tue, 22 Sep 2026 12:32:03 +0200 Subject: [PATCH 3/5] docs: use the singular, repeatable --event flag for listen (Codex review) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a81f7e5a..067d397e 100644 --- a/README.md +++ b/README.md @@ -616,9 +616,9 @@ never answers or declines on its own. already chatted with, `calls place` and `calls invite` exit with code 3 and `code: "number_restricted"` for anyone else; calls to existing contacts still work. A call WhatsApp refuses for that reason ends with - `reason: "call_restricted"` in `calls watch`. `polymorfa listen ---events session.restriction_updated` reports the restriction starting and - ending. + `reason: "call_restricted"` in `calls watch`. + `polymorfa listen --event session.restriction_updated` reports the + restriction starting and ending. - **Connections.** `calls leave` closes one connection, named by an 8 to 64 character ID from `A-Z`, `a-z`, `0-9`, `_`, and `-`. The call continues. `calls end` ends the call for everyone. From 5880cd3fa3df3779b340ce8104a4fcaa3a4ceea0 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Wed, 23 Sep 2026 12:22:49 +0200 Subject: [PATCH 4/5] Pin BanSafe SDK and verify restricted Calls behavior --- package-lock.json | 9 +++---- package.json | 2 +- test/commands/calls-sockets.test.ts | 20 ++++++++++++++++ test/commands/calls.test.ts | 37 +++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 31d2ef0f..ff07fcae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@modelcontextprotocol/server": "2.0.0", "@napi-rs/keyring": "1.3.0", "@oclif/core": "^4.13.5", - "@polymorfa/sdk": "0.1.0-dev.20260923093259", + "@polymorfa/sdk": "0.1.0-dev.20260923101121", "jq-wasm": "3.0.0-jq-1.8.2", "zod": "4.2.0" }, @@ -997,9 +997,10 @@ } }, "node_modules/@polymorfa/sdk": { - "version": "0.1.0-dev.20260923093259", - "resolved": "https://registry.npmjs.org/@polymorfa/sdk/-/sdk-0.1.0-dev.20260923093259.tgz", - "integrity": "sha512-rmSvbH1Tp6gGEPC0asXGaZnX8swxHniDzcK9XaQLTVkMciqjRxDDP+DQeA5jVI6cI3U08Paev15ek43ziCywFQ==", + "version": "0.1.0-dev.20260923101121", + "resolved": "https://registry.npmjs.org/@polymorfa/sdk/-/sdk-0.1.0-dev.20260923101121.tgz", + "integrity": "sha512-Oi40GMCvOJhhnR5OTw5SAFLbcFo62zMl8Zv3b11jEh5hY6L572VMDVnJQASz/LkQ9mVV694NjyXdIsywH2v/fA==", + "license": "MIT", "engines": { "node": ">=20" } diff --git a/package.json b/package.json index 964991d6..f1358aa3 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@modelcontextprotocol/server": "2.0.0", "@napi-rs/keyring": "1.3.0", "@oclif/core": "^4.13.5", - "@polymorfa/sdk": "0.1.0-dev.20260923093259", + "@polymorfa/sdk": "0.1.0-dev.20260923101121", "jq-wasm": "3.0.0-jq-1.8.2", "zod": "4.2.0" }, diff --git a/test/commands/calls-sockets.test.ts b/test/commands/calls-sockets.test.ts index 42235d17..4ee70637 100644 --- a/test/commands/calls-sockets.test.ts +++ b/test/commands/calls-sockets.test.ts @@ -142,6 +142,26 @@ describe('calls watch and media', () => { ) }) + test('keeps the BanSafe call restriction reason in watched call events', async () => { + server = await startCallsServer({ + onSocket: async (socket) => { + await socket.next() + socket.sendJson({participant: 'server:default', type: 'ready'}) + sendEvent(socket, 'call.received', 'call_3', { + from: '+15550103', + hasVideo: false, + }) + sendEvent(socket, 'call.ended', 'call_3', {reason: 'call_restricted'}) + }, + }) + + const result = await apiKey(() => + runCommand('calls watch --session support --count 2 --json'), + ) + expect(result.error).toBeUndefined() + expect(result.stdout).toContain('"reason":"call_restricted"') + }) + test('exits with an authentication error when the socket is refused with 4401', async () => { server = await startCallsServer({ onSocket: async (socket) => { diff --git a/test/commands/calls.test.ts b/test/commands/calls.test.ts index 9c955756..c1fd0090 100644 --- a/test/commands/calls.test.ts +++ b/test/commands/calls.test.ts @@ -182,6 +182,43 @@ describe('unified calls commands', () => { expect(server.requests).toHaveLength(2) }) + test('reports number restrictions for place and invite in human and JSON output', async () => { + server = await startHttpServer( + apiError( + 403, + 'number_restricted', + 'This number can call only people it has already chatted with.', + 'req_restricted', + ), + ) + + for (const command of [ + 'calls place +15550100 --session support', + 'calls invite call_1 +15550101', + ]) { + const human = await apiKey(() => runCommand(command)) + const json = await apiKey(() => runCommand(`${command} --json`)) + + expect(exitCode(human.error)).toBe(3) + expect(human.stderr).toContain('already chatted with') + expect(human.stderr).toContain('Request ID: req_restricted') + expect(exitCode(json.error)).toBe(3) + expect(JSON.parse(json.stderr)).toMatchObject({ + error: { + category: 'authorization', + code: 'number_restricted', + requestId: 'req_restricted', + }, + }) + } + expect(server.requests.map((request) => request.url)).toEqual([ + '/messaging/voip/calls', + '/messaging/voip/calls', + '/messaging/voip/calls/call_1/participants', + '/messaging/voip/calls/call_1/participants', + ]) + }) + test('reports an authentication failure with the credential redacted', async () => { server = await startHttpServer( apiError(401, 'invalid_credential', `Key ${API_KEY} is invalid.`), From 8a539610ec28697fc8e15b4d3f9970f1c5185349 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Wed, 23 Sep 2026 13:48:42 +0200 Subject: [PATCH 5/5] docs(cli): clarify call restriction contacts --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f4f11db9..8b8c1e14 100644 --- a/README.md +++ b/README.md @@ -683,8 +683,8 @@ never answers or declines on its own. ended. Declining ends the call for every participant. - **Number restrictions.** When WhatsApp restricts the Number to people it has already chatted with, `calls place` and `calls invite` exit with code 3 and - `code: "number_restricted"` for anyone else; calls to existing contacts still - work. A call WhatsApp refuses for that reason ends with + `code: "number_restricted"` for anyone else; calls to people it has already + chatted with still work. A call WhatsApp refuses for that reason ends with `reason: "call_restricted"` in `calls watch`. `polymorfa listen --event session.restriction_updated` reports the restriction starting and ending.