diff --git a/README.md b/README.md index e02b022a..078b5370 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,10 @@ Handwritten API clients, UI packages, and developer tooling for Polymorfa. The development branch contains the TypeScript server SDK, a framework-neutral browser runtime, shared UI contracts, Web Components, React bindings, thin Next.js server helpers, and a production-gated developer assistant. The exact Messaging and Platform sources are pinned in -`contracts/source.json`; the Graph-compatible catalog and Flow encryption -routes are pinned to the same API commit in `contracts/cloud-api-supplements.json`. Full Graph API parity -is outside this SDK's scope; `cloudCatalogs.list` and `flowEncryption` provide +`contracts/source.json`; the Graph-compatible catalog, marketing status and +Flow encryption routes are pinned to the same API commit in +`contracts/cloud-api-supplements.json`. Full Graph API parity is outside this +SDK's scope; `cloudCatalogs`, `cloudMarketing.status`, and `flowEncryption` provide typed clients for their respective Graph resources. The API includes an enrolled hosted message history beta. @@ -142,6 +143,7 @@ The handwritten Messaging resources in this milestone are: - `cloudTemplates`: list, retrieve, create, edit and delete Meta templates for a Number with an Official API connection; separate from project template drafts - `cloudCatalogs`: read WABA product-catalog links through the Graph facade +- `cloudMarketing`: read Meta's raw WABA Marketing Messages status strings without interpreting eligibility - `flowEncryption`: read or register a public Flow encryption key for one Meta phone number - `business`: manage the connected Business App profile, commerce catalog, products, collections, orders, compliance, linked accounts, and eligibility diff --git a/contracts/cloud-api-supplements.json b/contracts/cloud-api-supplements.json index eb316591..891d62d7 100644 --- a/contracts/cloud-api-supplements.json +++ b/contracts/cloud-api-supplements.json @@ -7,10 +7,15 @@ { "sourcePath": "docs/api-reference/openapi.graph.json", "sha256": "87e3a56aee42de041dd38db08d0b263123f84a5a4982ce0f20b780c3cfae3684", - "operationIds": ["graphListProductCatalogs", "graphListCatalogProducts"], + "operationIds": [ + "graphListProductCatalogs", + "graphListCatalogProducts", + "graphGetMarketingStatus" + ], "methods": [ "MessagingClient.cloudCatalogs.list", - "MessagingClient.cloudCatalogs.listProducts" + "MessagingClient.cloudCatalogs.listProducts", + "MessagingClient.cloudMarketing.status" ], "merged": true }, diff --git a/docs/internal/2026-09-26-marketing-status-consumer.md b/docs/internal/2026-09-26-marketing-status-consumer.md new file mode 100644 index 00000000..29abf9cc --- /dev/null +++ b/docs/internal/2026-09-26-marketing-status-consumer.md @@ -0,0 +1,23 @@ +# Raw Marketing Messages WABA status consumer + +`MessagingClient.cloudMarketing.status` consumes Polymorfa API PR #383 at +`1e1d613de599c2b4334f8de81fab087c74f31424`, with Graph OpenAPI copy +`docs/api-reference/openapi.graph.json` SHA-256 +`1270c8fb32a09cb717f3ea8edd6bd657e8ff3110e03a6286e68d5e16affff474`. +The exact operation is `graphGetMarketingStatus`. It reads raw optional WABA +status strings and preserves the API's Graph error behavior. No status value is +mapped to accepted terms, eligibility, insight availability, or a send decision. + +API: merged as polymorfa PR #383 and pinned at dev 29abb7b; not deployed by this SDK +change. SDKs: TypeScript server Messaging resource and public type updated; +other language clients in this workspace do not expose this Graph facade +resource and remain unaffected by the current contract. CLI: companion +read-only command is stacked separately and needs a published SDK artifact. +Docs: this SDK's overview and TypeScript page are updated; API public method +documentation remains in the API PR. Feature release: the existing Graph beta +gate is unchanged; installed SDK code is not customer availability. Admin: +no capability or staff state changes. + +No tests, typechecks, builds, documentation checks or live Meta calls were run +at the user's request. Publication, exact package integrity, CLI pin, and live +account acceptance remain separate gates. diff --git a/packages/typescript/README.md b/packages/typescript/README.md index 6760adfa..93752eae 100644 --- a/packages/typescript/README.md +++ b/packages/typescript/README.md @@ -414,6 +414,13 @@ from a catalog linked to that WABA. It has the same server-credential and to request the next page. The API rejects catalogs it cannot verify as linked; listing products does not grant permission to send them. +`messaging.cloudMarketing.status(wabaId, { version: "v26.0" })` reads Meta's +raw `marketing_messages_lite_api_status` and +`marketing_messages_onboarding_status` strings through an Official API Number +in the credential's project. It requires `sessions:read` and an organization +API key or project token. The fields do not establish terms acceptance, +recipient permission, eligibility, or permission to send. + `messaging.flowEncryption.retrieve(phoneNumberId, { version: "v26.0" })` reads the registered public key and Meta signature status with `sessions:read`. `messaging.flowEncryption.register(phoneNumberId, diff --git a/packages/typescript/src/index.ts b/packages/typescript/src/index.ts index eb29149d..6be63753 100644 --- a/packages/typescript/src/index.ts +++ b/packages/typescript/src/index.ts @@ -61,6 +61,11 @@ export { } from "./errors.js"; export { MessagingClient } from "./messaging/client.js"; export { CloudCatalogsResource } from "./messaging/cloud-catalogs.js"; +export { CloudMarketingResource } from "./messaging/cloud-marketing.js"; +export type { + CloudMarketingStatus, + GetCloudMarketingStatusParams, +} from "./messaging/cloud-marketing.js"; export { FlowEncryptionResource } from "./messaging/flow-encryption.js"; export type { FlowEncryptionKey, diff --git a/packages/typescript/src/messaging/client.ts b/packages/typescript/src/messaging/client.ts index 0ea465ef..7b7fe5b1 100644 --- a/packages/typescript/src/messaging/client.ts +++ b/packages/typescript/src/messaging/client.ts @@ -30,6 +30,7 @@ import { SessionsResource } from "./sessions.js"; import { TemplatesResource } from "./templates.js"; import { CloudTemplatesResource } from "./cloud-templates.js"; import { CloudCatalogsResource } from "./cloud-catalogs.js"; +import { CloudMarketingResource } from "./cloud-marketing.js"; import { FlowEncryptionResource } from "./flow-encryption.js"; import { UsersResource } from "./users.js"; import { VoipResource } from "./voip.js"; @@ -62,6 +63,7 @@ export class MessagingClient { readonly templates: TemplatesResource; readonly cloudTemplates: CloudTemplatesResource; readonly cloudCatalogs: CloudCatalogsResource; + readonly cloudMarketing: CloudMarketingResource; readonly flowEncryption: FlowEncryptionResource; readonly users: UsersResource; readonly voip: VoipResource; @@ -113,6 +115,10 @@ export class MessagingClient { credential.type, ); this.cloudCatalogs = new CloudCatalogsResource(transport, credential.type); + this.cloudMarketing = new CloudMarketingResource( + transport, + credential.type, + ); this.flowEncryption = new FlowEncryptionResource( transport, credential.type, diff --git a/packages/typescript/src/messaging/cloud-marketing.ts b/packages/typescript/src/messaging/cloud-marketing.ts new file mode 100644 index 00000000..33ffc43d --- /dev/null +++ b/packages/typescript/src/messaging/cloud-marketing.ts @@ -0,0 +1,47 @@ +import type { MessagingCredential } from "../credentials.js"; +import { PolymorfaConfigurationError } from "../errors.js"; +import { HttpTransport } from "../transport/http.js"; +import type { ApiResponse, RequestOptions } from "../transport/types.js"; + +/** Meta WABA observations. Neither string is an eligibility or terms grant. */ +export interface CloudMarketingStatus { + readonly id: string; + readonly marketing_messages_lite_api_status?: string; + readonly marketing_messages_onboarding_status?: string; +} + +export interface GetCloudMarketingStatusParams { + /** Meta Graph version, for example v26.0. */ + readonly version: string; +} + +export class CloudMarketingResource { + constructor( + private readonly transport: HttpTransport, + private readonly credentialType: MessagingCredential["type"], + ) {} + + /** Read raw WABA status fields through a project-owned Official API Number. */ + status( + wabaId: string, + params: GetCloudMarketingStatusParams, + options: RequestOptions = {}, + ): Promise> { + if (this.credentialType === "clientToken") { + throw new PolymorfaConfigurationError( + "Official API marketing status requires an organization API key or project token.", + "credential", + ); + } + if (!wabaId.trim() || !params.version.trim()) { + throw new PolymorfaConfigurationError( + "Provide a WABA ID and Graph version.", + ); + } + return this.transport.request({ + method: "GET", + path: `/graph/whatsapp/${encodeURIComponent(params.version)}/${encodeURIComponent(wabaId)}/marketing_messages/status`, + ...options, + }); + } +} diff --git a/packages/typescript/test/cloud-marketing.test.ts b/packages/typescript/test/cloud-marketing.test.ts new file mode 100644 index 00000000..f0e86a6b --- /dev/null +++ b/packages/typescript/test/cloud-marketing.test.ts @@ -0,0 +1,46 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { MessagingClient } from "../src/index.js"; +import { PROJECT_TOKEN } from "./support/credentials.js"; +import { startTestServer, type TestServer } from "./support/http-server.js"; +const servers: TestServer[] = []; +afterEach(async () => { + await Promise.all(servers.splice(0).map((server) => server.close())); +}); +describe("Official API marketing status", () => { + it("returns Meta's raw WABA status strings from the exact Graph route", async () => { + const payload = { + id: "102290129340398", + marketing_messages_lite_api_status: "ELIGIBLE", + }; + const server = await startTestServer(() => ({ + body: JSON.stringify(payload), + })); + servers.push(server); + const client = new MessagingClient({ + credential: { type: "projectToken", value: PROJECT_TOKEN }, + baseUrl: server.url, + }); + const status = await client.cloudMarketing.status("102290129340398", { + version: "v26.0", + }); + expect(status.data).toEqual(payload); + expect(server.requests[0]!.method).toBe("GET"); + expect(server.requests[0]!.path).toBe( + "/graph/whatsapp/v26.0/102290129340398/marketing_messages/status", + ); + }); + it("rejects client tokens and blank identifiers before dispatch", () => { + const browser = new MessagingClient({ + credential: { type: "clientToken", value: `pmfa_ct_${"A".repeat(94)}` }, + }); + expect(() => + browser.cloudMarketing.status("123", { version: "v26.0" }), + ).toThrow("organization API key or project token"); + const server = new MessagingClient({ + credential: { type: "projectToken", value: PROJECT_TOKEN }, + }); + expect(() => + server.cloudMarketing.status(" ", { version: "v26.0" }), + ).toThrow("WABA ID"); + }); +});