diff --git a/README.md b/README.md index c7bd81f1..e11fb38b 100644 --- a/README.md +++ b/README.md @@ -465,6 +465,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 --idempotency-key tpl-order-update @@ -504,6 +505,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..2a257da7 --- /dev/null +++ b/src/commands/cloud/marketing/status.ts @@ -0,0 +1,35 @@ +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) + } +} diff --git a/test/commands/cloud.test.ts b/test/commands/cloud.test.ts index 3bafb107..bf7477fd 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' @@ -54,9 +54,7 @@ describe('Official API commands', () => { }) test('submits bounded template JSON once', async () => { - directory = await mkdtemp( - join(homedir(), '.polymorfa-agent-work', '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(homedir(), '.polymorfa-agent-work', '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(homedir(), '.polymorfa-agent-work', '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(homedir(), '.polymorfa-agent-work', '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}}) @@ -431,6 +423,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', diff --git a/test/commands/flows.test.ts b/test/commands/flows.test.ts index 8e9594f9..48bcaf3c 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' @@ -88,9 +88,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-'), - ) + directory = await mkdtemp(join(tmpdir(), 'flows-cli-test-')) const path = join(directory, 'draft.json') await writeFile( path,