From fb39e851f1a62206c1dfe537d996c4b3e9aafb6f Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 26 Sep 2026 23:06:52 +0200 Subject: [PATCH 1/5] Add raw Marketing Messages status command --- README.md | 8 +++++ .../2026-09-26-marketing-status-command.md | 22 +++++++++++++ src/commands/cloud/marketing/status.ts | 32 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 docs/internal/2026-09-26-marketing-status-command.md create mode 100644 src/commands/cloud/marketing/status.ts diff --git a/README.md b/README.md index a47b43d4..18fd786a 100644 --- a/README.md +++ b/README.md @@ -386,6 +386,7 @@ beta feature. ```shell polymorfa cloud catalog list --waba WABA_ID --graph-version v26.0 --limit 25 polymorfa cloud catalog products --waba WABA_ID --catalog CATALOG_ID --graph-version v26.0 --limit 25 +polymorfa cloud marketing status --waba WABA_ID --graph-version v26.0 polymorfa cloud template list --session number_123 polymorfa cloud template get order_update --session number_123 --language en_US polymorfa cloud template create --session number_123 --input template.json @@ -421,6 +422,13 @@ availability from one catalog linked to the authenticated WABA. Pass its unlinked catalogs and simulated Numbers. This read does not send a product or establish that the merchant may sell it. +`cloud marketing status` uses `sessions:read` to return Meta's raw WABA +`marketing_messages_lite_api_status` and +`marketing_messages_onboarding_status` strings when present. The values do not +establish accepted terms, recipient permission, eligibility, or a right to +send. Simulated Numbers and unsupported accounts return API errors; the CLI +does not convert status strings into an eligibility decision. + Reauthorization creates a QuickLink for a stopped or disconnected standalone Official Number. It never stops the Number or opens the link automatically. Running Numbers return a conflict; Hybrid reauthorization is unsupported. diff --git a/docs/internal/2026-09-26-marketing-status-command.md b/docs/internal/2026-09-26-marketing-status-command.md new file mode 100644 index 00000000..b1ffe2ff --- /dev/null +++ b/docs/internal/2026-09-26-marketing-status-command.md @@ -0,0 +1,22 @@ +# Raw Marketing Messages status command gate + +`cloud marketing status` consumes `MessagingClient.cloudMarketing.status` from +`polymorfa/sdks` PR #315, stacked on SDK PR #314. Its API source is +`polymorfa/polymorfa` PR #383 at +`1e1d613de599c2b4334f8de81fab087c74f31424`; the Graph OpenAPI copy has +SHA-256 `1270c8fb32a09cb717f3ea8edd6bd657e8ff3110e03a6286e68d5e16affff474`. +The CLI renders the exact raw status object and does not interpret terms, +eligibility, insight access or a send decision. + +The CLI retains its older exact SDK registry pin in `package.json` and +`package-lock.json`. This source change cannot be published or released until +the API and SDK PR stacks merge, the SDK is published and its exact version and +integrity verified, and that version is pinned here with a regenerated +lockfile. No registry or production setting was changed. + +API: separate PR #383; SDKs: typed TypeScript consumer in a separate stacked +PR; CLI: new read-only command and help; Docs: CLI README plus API/SDK public +documentation in their respective PRs; Feature release: existing Graph beta +gate remains off by default, no enrollment or entitlement change; Admin: +unaffected. No tests, typechecks, builds, documentation checks or live Meta +requests were run at the user's request. diff --git a/src/commands/cloud/marketing/status.ts b/src/commands/cloud/marketing/status.ts new file mode 100644 index 00000000..0cfdaf07 --- /dev/null +++ b/src/commands/cloud/marketing/status.ts @@ -0,0 +1,32 @@ +import {Flags} from '@oclif/core' +import {PolymorfaConfigurationError} from '@polymorfa/sdk' +import {BaseCommand} from '../../../base-command.js' + +export default class CloudMarketingStatus extends BaseCommand { + static override description = + 'Read raw Marketing Messages WABA status strings (beta). Requires sessions:read and Graph access.' + + static override flags = { + waba: Flags.string({ + description: 'WhatsApp Business Account ID from an Official API Number in your project.', + required: true, + }), + 'graph-version': Flags.string({ + description: 'Meta Graph version, for example v26.0.', + required: true, + }), + } + + async run(): Promise { + const {flags} = await this.parse(CloudMarketingStatus) + if (!flags.waba.trim() || !flags['graph-version'].trim()) + throw new PolymorfaConfigurationError('Provide a WABA ID and Graph version.') + const client = await this.messagingClient(flags, { + serverCredentialOnly: 'Official API', + }) + const response = await client.cloudMarketing.status(flags.waba, { + version: flags['graph-version'], + }) + this.writeValue(response, JSON.stringify(response.data, null, 2), flags) + } +} From 43b8a7c50e0c58d2c27a2027472838db22593506 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 26 Sep 2026 23:34:09 +0200 Subject: [PATCH 2/5] style(cli): format marketing status command --- src/commands/cloud/marketing/status.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/commands/cloud/marketing/status.ts b/src/commands/cloud/marketing/status.ts index 0cfdaf07..2a257da7 100644 --- a/src/commands/cloud/marketing/status.ts +++ b/src/commands/cloud/marketing/status.ts @@ -8,7 +8,8 @@ export default class CloudMarketingStatus extends BaseCommand { static override flags = { waba: Flags.string({ - description: 'WhatsApp Business Account ID from an Official API Number in your project.', + description: + 'WhatsApp Business Account ID from an Official API Number in your project.', required: true, }), 'graph-version': Flags.string({ @@ -20,7 +21,9 @@ export default class CloudMarketingStatus extends BaseCommand { async run(): Promise { const {flags} = await this.parse(CloudMarketingStatus) if (!flags.waba.trim() || !flags['graph-version'].trim()) - throw new PolymorfaConfigurationError('Provide a WABA ID and Graph version.') + throw new PolymorfaConfigurationError( + 'Provide a WABA ID and Graph version.', + ) const client = await this.messagingClient(flags, { serverCredentialOnly: 'Official API', }) From 0b3ca14f55dab2c423a04d3f517131703d168856 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Thu, 1 Oct 2026 16:29:39 +0300 Subject: [PATCH 3/5] Test marketing status command --- test/commands/cloud.test.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/test/commands/cloud.test.ts b/test/commands/cloud.test.ts index 3bafb107..0590e5d7 100644 --- a/test/commands/cloud.test.ts +++ b/test/commands/cloud.test.ts @@ -431,6 +431,41 @@ describe('Official API commands', () => { expect(server.requests).toHaveLength(0) }) + test('reads raw Marketing Messages status strings without interpretation', async () => { + const data = { + id: '1234', + marketing_messages_onboarding_status: 'ONBOARDED', + } + server = await startHttpServer({body: data}) + const result = await credential( + () => + runCommand( + 'cloud marketing status --waba 1234 --graph-version v26.0 --json', + ), + 'project', + ) + expect(result.error).toBeUndefined() + expect((JSON.parse(result.stdout) as {data: unknown}).data).toEqual(data) + expect(server.requests).toHaveLength(1) + expect(server.requests[0]).toMatchObject({ + method: 'GET', + url: '/graph/whatsapp/v26.0/1234/marketing_messages/status', + }) + }) + + test('refuses marketing status for a client token before any request', async () => { + server = await startHttpServer({body: {}}) + const result = await credential( + () => + runCommand( + 'cloud marketing status --waba 1234 --graph-version v26.0 --json', + ), + 'client', + ) + expect(result.error).toBeDefined() + expect(server.requests).toHaveLength(0) + }) + async function credential( run: () => Promise, kind: 'api' | 'project' | 'client' = 'api', From 97372af704de1386c8372ee49ecfeaae067701d5 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Thu, 1 Oct 2026 16:37:56 +0300 Subject: [PATCH 4/5] Create command test fixtures in the system temp directory The cloud and Flow tests wrote under ~/.polymorfa-agent-work, which exists on CI only if the Functions tests ran first. --- test/commands/cloud.test.ts | 10 +++++----- test/commands/flows.test.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/commands/cloud.test.ts b/test/commands/cloud.test.ts index 0590e5d7..3d1a5bd3 100644 --- a/test/commands/cloud.test.ts +++ b/test/commands/cloud.test.ts @@ -1,6 +1,6 @@ import {mkdir, mkdtemp, rm, writeFile} from 'node:fs/promises' import {AsyncEntry} from '@napi-rs/keyring' -import {homedir} from 'node:os' +import {tmpdir} from 'node:os' import {join} from 'node:path' import {runCommand} from '@oclif/test' import {afterEach, describe, expect, test, vi} from 'vitest' @@ -55,7 +55,7 @@ describe('Official API commands', () => { test('submits bounded template JSON once', async () => { directory = await mkdtemp( - join(homedir(), '.polymorfa-agent-work', 'cloud-cli-test-'), + join(tmpdir(), 'cloud-cli-test-'), ) const path = join(directory, 'template.json') const body = { @@ -87,7 +87,7 @@ describe('Official API commands', () => { test('uses the explicit template project when a different directory is linked', async () => { directory = await mkdtemp( - join(homedir(), '.polymorfa-agent-work', 'cloud-cli-test-'), + join(tmpdir(), 'cloud-cli-test-'), ) await writeFile( join(directory, 'polymorfa.json'), @@ -153,7 +153,7 @@ describe('Official API commands', () => { test('edits a selected template language and reports acceptance', async () => { directory = await mkdtemp( - join(homedir(), '.polymorfa-agent-work', 'cloud-cli-test-'), + join(tmpdir(), 'cloud-cli-test-'), ) const path = join(directory, 'edit.json') const body = {components: [{type: 'BODY', text: 'Ready'}]} @@ -184,7 +184,7 @@ describe('Official API commands', () => { test('rejects an edit containing a renamed template before dispatch', async () => { directory = await mkdtemp( - join(homedir(), '.polymorfa-agent-work', 'cloud-cli-test-'), + join(tmpdir(), 'cloud-cli-test-'), ) const path = join(directory, 'invalid.json') await writeFile(path, JSON.stringify({name: 'renamed', components: [{}]})) diff --git a/test/commands/flows.test.ts b/test/commands/flows.test.ts index 8e9594f9..d4a86efe 100644 --- a/test/commands/flows.test.ts +++ b/test/commands/flows.test.ts @@ -1,5 +1,5 @@ import {mkdtemp, rm, writeFile} from 'node:fs/promises' -import {homedir} from 'node:os' +import {tmpdir} from 'node:os' import {join} from 'node:path' import {runCommand} from '@oclif/test' import {afterEach, describe, expect, test} from 'vitest' @@ -89,7 +89,7 @@ describe('project Flow commands', () => { }) test('rejects project override in a draft file before dispatch', async () => { directory = await mkdtemp( - join(homedir(), '.polymorfa-agent-work', 'flows-cli-test-'), + join(tmpdir(), 'flows-cli-test-'), ) const path = join(directory, 'draft.json') await writeFile( From 8674bb96b2334f585b2aae68c291d87a93335e39 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Thu, 1 Oct 2026 16:38:17 +0300 Subject: [PATCH 5/5] Format command tests --- test/commands/cloud.test.ts | 16 ++++------------ test/commands/flows.test.ts | 4 +--- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/test/commands/cloud.test.ts b/test/commands/cloud.test.ts index 3d1a5bd3..bf7477fd 100644 --- a/test/commands/cloud.test.ts +++ b/test/commands/cloud.test.ts @@ -54,9 +54,7 @@ describe('Official API commands', () => { }) test('submits bounded template JSON once', async () => { - directory = await mkdtemp( - join(tmpdir(), 'cloud-cli-test-'), - ) + directory = await mkdtemp(join(tmpdir(), 'cloud-cli-test-')) const path = join(directory, 'template.json') const body = { name: 'order_update', @@ -86,9 +84,7 @@ describe('Official API commands', () => { }) test('uses the explicit template project when a different directory is linked', async () => { - directory = await mkdtemp( - join(tmpdir(), 'cloud-cli-test-'), - ) + directory = await mkdtemp(join(tmpdir(), 'cloud-cli-test-')) await writeFile( join(directory, 'polymorfa.json'), JSON.stringify({ @@ -152,9 +148,7 @@ describe('Official API commands', () => { }) test('edits a selected template language and reports acceptance', async () => { - directory = await mkdtemp( - join(tmpdir(), 'cloud-cli-test-'), - ) + directory = await mkdtemp(join(tmpdir(), 'cloud-cli-test-')) const path = join(directory, 'edit.json') const body = {components: [{type: 'BODY', text: 'Ready'}]} await writeFile(path, JSON.stringify(body)) @@ -183,9 +177,7 @@ describe('Official API commands', () => { }) test('rejects an edit containing a renamed template before dispatch', async () => { - directory = await mkdtemp( - join(tmpdir(), 'cloud-cli-test-'), - ) + directory = await mkdtemp(join(tmpdir(), 'cloud-cli-test-')) const path = join(directory, 'invalid.json') await writeFile(path, JSON.stringify({name: 'renamed', components: [{}]})) server = await startHttpServer({body: {success: true}}) diff --git a/test/commands/flows.test.ts b/test/commands/flows.test.ts index d4a86efe..48bcaf3c 100644 --- a/test/commands/flows.test.ts +++ b/test/commands/flows.test.ts @@ -88,9 +88,7 @@ describe('project Flow commands', () => { }) }) test('rejects project override in a draft file before dispatch', async () => { - directory = await mkdtemp( - join(tmpdir(), 'flows-cli-test-'), - ) + directory = await mkdtemp(join(tmpdir(), 'flows-cli-test-')) const path = join(directory, 'draft.json') await writeFile( path,