From 254f71b19ffca87be9774185f6ed989b9e60fc1f Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Tue, 20 Oct 2026 01:01:57 +0300 Subject: [PATCH 1/9] Use Number-scoped test phone and event routes --- CHANGELOG.md | 976 +--- contracts/README.md | 780 +-- contracts/coverage.json | 4858 +++++++++-------- contracts/openapi.messaging.json | 1402 +++-- contracts/test-number-dropin-routes.json | 1590 ++++++ contracts/testing-events.json | 638 ++- contracts/testing-phone.json | 156 +- packages/typescript/README.md | 478 +- .../typescript/src/messaging/onboarding.ts | 92 +- .../src/messaging/testing-configuration.ts | 4 +- packages/typescript/test/onboarding.test.ts | 2 +- .../typescript/test/testing-events.test.ts | 145 +- .../typescript/test/testing-phone.test.ts | 51 +- 13 files changed, 5826 insertions(+), 5346 deletions(-) create mode 100644 contracts/test-number-dropin-routes.json diff --git a/CHANGELOG.md b/CHANGELOG.md index cef7379f..d7cd89a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,972 +2,12 @@ ## Unreleased -- Prepare direct server passkey challenge, assertion and explicit confirmation methods in all six SDKs. Writes use one attempt; ceremony execution remains external. Bound to accepted API source `77e9ce31`; unreleased until package publication. -- Add the read-only nullable call-settings exclusion-end response field across all six SDKs. Rust response struct literals require the new optional field; C# positional construction remains compatible. Bound to accepted API source `77e9ce31`, which also clarifies when a ban is excluded; unreleased. - -- TypeScript, Python and PHP call-settings updates now refuse keys the API does not accept, including the response-only `simultaneousCallsCoverageExcludedUntil`, before sending. Go, Rust and .NET requests already serialize only writable fields. - -- All six server SDKs expose optional automatic-pause `reason`/`code` and - optional recipient-skip `messageIndex`/`followUpId` from accepted API source - `9bb1e2ed`. Future reason/code strings and raw envelope fields are retained; - manual omission and zero sequence positions are preserved. Pause timestamps - remain seconds, skip timestamps milliseconds. Rust full struct literals and - Go unkeyed literals need the added optional fields; C# constructor/Deconstruct - arities are unchanged. Source-only and unpublished; see - `docs/campaign-webhook-metadata.md` for compatibility and acceptance gates. -- Prepare simultaneous-call settings and Ban Insurance evidence in all six - server SDKs against accepted API #559 source `c1a43a1e`. Updates preserve - omission and explicit false. The API enforces acknowledgment and team-key - authority on an off-to-on transition. Claims and webhooks recognize - `simultaneous_calls`; claim evidence includes `simultaneousCalls`. - Package publication and deployed acceptance remain gated. -- Official API round-2 betas now have native source counterparts in all six - server SDKs: Number catalog-cache operations, marketing status and lifecycle - events, parent/previous identity fields, incoming carts, multipart Official - group pictures, Platform batching settings and whole-body signed batch - receivers. Graph resources and Linked Devices groups retain their own routes. - These changes are source-only. Exact combined-source hosted qualification, - accepted downstream source, API acceptance and publication remain required. - -- All six server SDKs submit each direct media grant, API relay upload, history - chunk retry and history media deletion at most once per invocation, including - when a caller supplies an idempotency key and a positive retry option. These - held routes have no server replay ledger. Errors retain their metadata; - established message-send/GET retries and direct CDN recovery are unchanged. - This correction is source-only and does not release the held API routes. - -- Project event streams in TypeScript, Go, Python, PHP and .NET retain their - resume cursor until the event payload validates. TypeScript now validates the - decoded webhook envelope. Their parsers enforce 4 MiB UTF-8 frame budgets - across incomplete lines, multiline data and comments. Rust's existing cursor - validation and frame bound gain regression coverage. Native malformed-payload - error and reconnect policies remain unchanged. -- Go and .NET storage synchronization preserve the configured initial cursor - when no checkpoint exists. A saved checkpoint still takes precedence; event - commit, checkpoint CAS and manual ACK retain their order. These repairs are - source-only and unreleased. - -- Webhook phone offline start day. Platform and Messaging webhook create, - update and read types gain `phoneOfflineStartDay` (7-13, default 10): the - first day without the phone on which the webhook receives - `session.phone_offline`. Platform webhooks always return it. To stop the - warning, leave the event out of `eventTypes` (Platform) or `events` - (Messaging). `PHONE_OFFLINE_START_DAY` exports the bounds. Not usable until - the API release that adds the field is deployed. API contract: - polymorfa/polymorfa#631. -- Receipt-timed campaign follow-ups (beta). Messaging and Platform campaign - create and update accept `followUp` (`CampaignFollowUp`): one text or - template message sent from the first message's number, timed from its first - delivery or read. Campaigns return the stored follow-up, recipients return - `followUp` (`CampaignFollowUpSummary`), and campaign analytics return - `followUps` (`CampaignFollowUpReport`). `null` removes the follow-up on an - unlaunched draft. A team that is not enrolled receives - `403 feature_unavailable`. API contract: polymorfa/polymorfa#503. -- Campaign A/B tests (beta). Messaging and Platform campaign create and - update accept `variants` (`CampaignVariant`, two to four) and - `variantStrategy` (`CampaignVariantStrategy`); campaigns return them with - `experimentOutcome`. Campaign analytics include `experiment` results by - variant, and Platform `campaigns.analytics` now returns - `PlatformCampaignAnalytics` instead of an untyped object. A team that is not - enrolled receives `403 feature_unavailable`. API contract: - polymorfa/polymorfa#406. -- Weighted message variation (beta). Messaging and Platform campaign create - and update accept `messageVariations`, two to five weighted alternatives - (`CampaignMessageVariation`), and campaigns return it. `null` clears the - alternatives on an unlaunched draft. A team that is not enrolled receives - `403 feature_unavailable`. API contract: polymorfa/polymorfa#409. -- `Client.requestLogs` reads a project's API request log (`logs:read`): - `list` with status, method, route, source, credential, request ID and trace - ID filters, `follow` for newer requests, and `tail`, an async iterator that - polls for new requests and honours `Retry-After`. Not usable until the API - release that adds the request log is deployed. -- Test number phone controls. `MessagingClient.testing` adds `getPhone` - (online state and linked companions), `sendPhoneMessage` (a text from the - simulated phone to another Test number in the project) and - `unlinkPhoneDevice`. Requires Test numbers access. API contract: - polymorfa/polymorfa#526. - -- Dynamic WhatsApp Flow endpoints (beta, Cloud onboarding enrollment, Official - API Numbers only). `Client.project(projectId).flows` adds `endpoint`, - `setEndpoint` (modes `forward`, `function` and `direct`; a forward endpoint's - `signingSecret` is returned once), `deleteEndpoint`, `endpointReceipts` - (metadata only), `encryptionKey` and `rotateEncryptionKey` (managed key - custody per Number). Writes make one attempt. New - `verifyFlowForwardSignature` and `FLOW_FORWARD_SIGNATURE_HEADER` verify - forwarded requests at your endpoint. Contracts are pinned to API `dev` - `171a968`. API contract: polymorfa/polymorfa#428 and #434. - -- Added source-build `Client.analytics.metrics` for scoped Prometheus/OpenMetrics gauges, including denominators, observation times and coverage. Uses Analytics opt-in and `sessions:read`. - -- `Client.analytics.get` reads WhatsApp number activity, completed 24-hour read and reply cohorts, response times and connection vitals. Requires `sessions:read` and Analytics enabled by a team owner or admin. Missing coverage returns null rates. - -- Hybrid Link tier transitions. `Client.sessions.quoteTierChange` accepts - `hybridResolution` (`keep` one connection or `split` into two Numbers) for a - Hybrid Link Number leaving Pro, or `hybridMerge` to merge a same-number pair - on an upgrade to Pro. `NumberTierChange.quote.hybridTransition` reports the - plan and its `status`. `Client.projects.listHybridMergeCandidates` lists - mergeable pairs. `PolymorfaErrorCode` adds `hybrid_choice_required` and - `hybrid_transition_ineligible`. -- `Session` gains optional `newChatCapping` (`NewChatCapping`): WhatsApp's - per-number new-chat cap, returned by `sessions.retrieve` for linked-device - numbers. API contract: polymorfa/polymorfa#403. -- Brazil payment orders (beta, requires team enrollment, an Official API - Number and Meta payments eligibility in Brazil). `messages.send` accepts - `orderDetails` (Pix dynamic code, payment link or boleto, itemized or - total only) and `orderStatus` content. New types: - `OrderDetailsMessageContent`, `OrderStatusMessageContent`, - `OrderPaymentSettings`, `OrderDetailsItemization`, `PixDynamicCodePayment`, - `PaymentOrderAmount`, `PixKeyType`, `OrderStatus`, `OrderPaymentStatus`, - `SendOrderDetailsMessageRequest`, `SendOrderStatusMessageRequest` and - `OrderPaymentUpdatedPayload` (discriminated on `kind`) for the - `order.payment_updated` event. `MessageKind` adds `order_details` and - `order_status`. New error - codes: `order_status_transition_invalid` and `order_cancellation_failed`. - Polymorfa relays orders and payment reports; it does not process funds. - Contracts are pinned to API `dev` `e818ba6`. -- Official groups (beta, requires team enrollment and an Official Business - Account). `MessagingClient.officialGroups` adds `list`, `create`, `retrieve`, - `update`, `delete`, `getInviteLink`, `resetInviteLink`, `removeParticipants`, - `listJoinRequests`, `approveJoinRequests`, `rejectJoinRequests` and `pin`. - Changes make one attempt and pass `idempotencyKey` through. New error codes: - `whatsapp_groups_ineligible`, `whatsapp_group_creation_paused`, - `whatsapp_group_limit_reached`, `whatsapp_group_full`, - `whatsapp_group_suspended` and `whatsapp_group_has_no_participants`. - `GroupUpdatePayload` and `GroupParticipantPayload` add optional Official - group fields. Contracts are pinned to API `dev` `ee217bf`. -- Campaign conversion reporting (beta, requires team enrollment). - `Client.campaigns.recordConversion(campaignId, body)` reports a conversion - for a named campaign recipient, deduplicated by `eventId`. - `Client.campaigns.conversions(campaignId, { projectId })` returns counts by - attribution outcome and reported value per currency. New types: - `RecordCampaignConversionRequest`, `CampaignConversion`, - `CampaignConversionValue`, `CampaignConversionOutcome`, - `CampaignConversionReport` and `CampaignConversionCurrencyTotal`. -- Campaign types include `sendWindow`. `Campaign.sendWindow` is a - `CampaignSendWindow` or null, and campaign create and update requests on - both clients accept a `CampaignSendWindowRequest`; update accepts null to - remove the window. `Client.campaigns.list`, `retrieve`, `create` and - `update` return `PlatformCampaign` instead of an untyped payload. -- `QuickLinkPurpose` includes `reauthorization`. Hosted history messages - expose `mediaRetrieval`, call participants expose optional `handRaised`, and - `message.ack` payloads expose the optional Official API `pricing` - classification (`MetaPricingReport`). -- Official API consumers: Number-scoped Meta templates (`cloudTemplates`), - service windows, Meta pricing counts, Cloud credential health and - reauthorization, QuickLink sync receipts, typing with an inbound message ID, - the Flow provider lifecycle, linked catalogs and Flow encryption keys, and - the Meta lifecycle webhook payload variants. Provider writes make one - attempt. Contracts are pinned to API `dev` `29abb7b`. -- Campaign records type the stored `sendWindow`; create and update requests - accept `CampaignSendWindowRequest`. -- Breaking for Flow drafts: `flows.list` and `flows.retrieve` now return the - unwrapped record in `response.data`, like the other Flow methods. Replace - `response.data.data` with `response.data`. - -- `message.received` payload types include `replyChoice` (`kind` `button` or - `list` and the `id` you assigned) and `parentMessageId`, and the linked-device - message `type` union adds `button_reply`, `list_reply`, and - `native_flow_response`. `ReplyChoice` is exported. - -- Calls quality diagnostics respect their five-second send interval when a - connection closes, avoiding an extra report that the API can rate-limit. - -- Browser Calls uses its authenticated lifecycle socket for ICE candidates and - pauses remote candidate polling while connected. It falls back to HTTP when - that socket disconnects. The Calls example also displays call-control errors - and failed request IDs. -- Team webhook tests accept an optional event type. The SDK now rejects a - supplied body or session ID before making that request. Project webhook - tests keep the paired native body and session ID input. - -- Retired the unproduced `bansafe.risk_changed`, - `bansafe.health_changed`, and `bansafe.enforcement` webhook names and their - test-event fixtures. The typed `bansafe.incident`, `bansafe.action`, - `bansafe.health_threshold`, and `bansafe.claim` events remain available. - -- Added `Client.sessions.getCapabilities(sessionId)` and the - `SessionCapabilities` types. It reads which WhatsApp features and limits - WhatsApp has enabled for a number, as of the number's last configuration sync; - `status` is `unknown` and every value null before the first sync. It needs - `sessions:read` and enrollment in the number capabilities beta (403 until - then). -- Browser Calls hold locally gathered ICE candidates until the platform - answers the call's offer, then send them. Candidates sent earlier were - refused with `409` because no media session existed yet, and were lost. -- Fixed browser requests failing before they were sent. `BrowserTransport` - called the page's `fetch` with the transport as its receiver, which browsers - reject with `TypeError: Illegal invocation`. Every request, including Calls - placement, Answer, and Reject, surfaced as a `connection` error and no - request reached the network. `HttpTransport` had the same defect in browsers, - Cloudflare Workers, and Deno. Both transports now call the default or supplied - `fetch` without a receiver. -- Added typed hosted message history reads to `MessagingClient.chats`: - `list`, `retrieve`, `listMessages`, and `retrieveMessage`. Server credentials - need the relevant read scope, HMS enabled on the Number, and enrollment in - the message history beta. Pages retain both cursors and responses expose the - data region. `PolymorfaErrorCode` includes `hms_not_enabled`. -- Added `Client.events.listIndexed()` to follow event ingestion offsets, - including late-indexed events with older producer timestamps. - -- Call consent. `Client.callPolicy` (`retrieve`, `update`) reads and replaces - the team's blocked country codes with an optional `expectedRevision` guard, - and `Client.callOptOuts` (`list`, `create`, `import`, `delete`) manages the - team's do-not-call list. Both need an team key. `list` returns a - `CursorPage` that follows `page.nextCursor`; `create` answers `201` for a new - entry and `200` for one already listed, told apart by `metadata.status`. - `MessagingClient.voip.retrieveCallPermission()` reads a person's call - permission on a Cloud API Number, and `MessagingClient.voip.check()` runs a - placement's checks without placing a call. Message content adds - `callPermissionRequest: { body }` in both server and browser clients; the - browser kind is `call_permission_request`, with existing client-token grants - and server consent checks. The `call.permission_changed` webhook - event narrows to `CallPermissionChangedPayload`. `PolymorfaErrorCode` adds - `call_recipient_opted_out`, `call_destination_blocked`, - `call_permission_request_limited`, `call_permission_granted` and - `call_opt_out_limit`, and `PolymorfaRateLimitReason` adds - `call_permission_request`. -- Signed Voice upload errors keep the HTTP status but discard storage response - text and headers that could expose the upload URL. - -- Added `Client.voice.audio` for scoped audio upload, synthesis, preview, - retention and deletion, plus `Client.voice.providerCredentials` for write-only - provider keys. Typed `voice.asset_ready` and `voice.asset_failed` webhooks - report project-scoped outcomes. The Voice Automation beta still requires - server enrollment and deployment. - -- `constructWebhookEvent` accepts project-scoped deliveries with an empty - `session`, including Voice asset events. - -- Added `Client.usage` to read monthly metered usage, page or iterate usage - records, and inspect team usage gates. Project clients can read only their - own usage; gate state requires an team key. The `usage.recorded` - webhook has a typed payload, and enforced limits use the - `gate_limit_reached` error code. Usage is measured but not charged. - -- Added `Client.calls` for call analytics and call detail records (needs - `sessions:read`; also on project clients). `stats()` returns totals, - per-day, per-hour, per-number or per-outcome groups, and a 168-cell - hour-of-week heatmap in an IANA time zone. `list()` returns a - `CursorPage`. `export()` returns one CSV or NDJSON page of up to - 1,000 records with its `nextCursor`, and `exportAll()` yields every page, - dropping repeated CSV header rows. `exportAll()` throws an - `invalid_response` `PolymorfaServerError`, without yielding the page, when - the API returns an already requested cursor. Filters: `projectId` (team clients), - `sessionId`, `direction`, `upstream`, `outcome`, `since` and `until`. Call - records identify the other party only by the pseudonymous `peerRef`. - Response metadata now keeps the `polymorfa-next-cursor` header. -- Added `Client.callRetention` with `retrieve()` and `update()` for the - team's call data retention (`GET` and `PUT /platform/call-retention`), and - the `CallRetention`, `CallRetentionPolicy`, and `UpdateCallRetentionRequest` - types. `UpdateCallRetentionRequest` is a union on `policy`: `custom` - requires `retentionDays`, and named policies may omit it. Team API keys and project tokens can read the setting; only team API - keys can change it. Deletion of call data older than the period starts on - a date Polymorfa announces; until then the setting records a choice and - nothing is deleted. - -- Breaking: `Client.campaigns.create` requires a `CreatePlatformCampaignRequest` - body with `name` and `projectId`. Named optional fields replace the open - top-level object; composer, messages, audience, compliance and variant JSON - values remain opaque. - -- Breaking: `Client.campaigns.recipients` returns a cursor page - (`{ data, page }`) instead of a bare array, takes `projectId`, `status`, - `cursor` and `limit`, and each recipient now carries `sentAt`, - `deliveredAt`, `readAt`, `failedAt` and `respondedAt`. Iterate - `response.data.data` and continue from `response.data.page.nextCursor`. - -- Breaking: `MessagingClient.campaigns.stop` returns `CampaignStopResponse`. - Its `operationId` is `string | null`: stopping a campaign with no active - delivery run cancels it immediately and answers with `null`. Code that read - `operationId` as a string must handle `null`. - -- Breaking: `Client.audiences.create` takes `CreateAudienceRequest` instead of - an open `PlatformPayload`. It accepts inline `members` - (`CreateAudienceFromMembers`), or `fileId` with the required `mapping` for a - spreadsheet import (`CreateAudienceFromFile`), never both; omitting both - creates an empty audience. It returns the audience with its import counts and - up to 20 rejected rows. - -- Breaking: `Client.campaigns.retrieve`, `delete`, `analytics` and `events` take - a required `PlatformCampaignParams` argument before their request options, - and `update` requires it after the body. Set its mandatory `projectId` to the - project that owns the campaign. This resource belongs to the organization - client; a team API key is not bound to one project. - -- `Client.campaigns.update` takes `UpdatePlatformCampaignRequest`. It names - `recipientListId` (string or null), which points an unlaunched draft at - another audience or detaches it, and keeps an index signature because the - contract still declares the body as an open object. The API refuses the - change with `409` once the campaign has launched or its audience has been - copied into recipients; the SDK raises `PolymorfaConflictError`. - -- Added campaign recipients on both surfaces. - `MessagingClient.campaigns.listRecipients` and `addRecipients`, and - `Client.campaigns.addRecipients`, append up to 1,000 recipients to a campaign - that has not started sending and report duplicate and invalid entries. - `CreateCampaignRequest` accepts inline `recipients`. - -- `MessagingClient.campaigns.addRecipients`, `Client.campaigns.addRecipients` - and `Client.audiences.addMembers` send each request once. The API does not - replay these appends, so the SDK generates no idempotency key and turns off - automatic retries for them, even when you pass `idempotencyKey`. A retry - after a lost response would report the rows the first attempt added as - duplicates. List the recipients or members before you append again. To opt - back in, set both `maxNetworkRetries` and `idempotencyKey` on the request. - -- Messaging and Platform campaign launch, pause, resume, and stop now generate - an idempotency key when the caller omits one. Automatic retries reuse the - same key under the API's lifecycle replay contract. - -- Added `Client.audiences.addMembers`, `listMembers` and `deleteMember` for - audience membership. - -- Added `Client.optOuts.getSettings` and `updateSettings` for the - organization's STOP/START keyword capture. - -- Added `campaign_throughput_capped` to `POLYMORFA_ERROR_CODES`. Campaign - launch and resume refuse with it when the project is over its send rate. - -- Added the `contact.opted_out` and `contact.opted_in` webhook events with the - exported `ContactOptPayload`. - -- `KnownPolymorfaErrorCode` adds `addon_required` for QuickLink settings - that require an active add-on, and continues recognizing legacy `premium_required` responses. The - current API contract no longer advertises `premium_required`. -- Calls: `PolymorfaErrorCode` adds `number_restricted`. Placing a call or - inviting someone the Number has never chatted with fails with it while - WhatsApp restricts the Number to existing contacts; `Retry-After` carries the - seconds until the restriction ends when WhatsApp reports one. A call WhatsApp - refuses for that reason ends with `reason: "call_restricted"`. The Calls - client preserves this value in `Call.endReason` and both `ended` events. -- Webhooks: new `session.restriction_updated` event and - `SessionRestrictionUpdatedPayload` (`type`, `active`, `enforcementType`, - `expiresAt`, `observedAt`). `SessionLoggedOutPayload.reason` is now - `"banned" | "device_removed" | "unknown"` with WhatsApp's `code`, instead of - a free-form string. -- Test events: `TEST_EVENT_FIXTURES` adds `session.restriction_updated` with - the `restrictionActive` override, and `callEndReason` accepts - `call_restricted`. -- `Client.sipTrunks.endpoint()` returns the SIP address your PBX points at. - `SipEndpoint` is a union on `status`: `SipEndpointHosted` carries the - `host`, the `transports` (`SipEndpointTransport`, with port and SRTP - policy) and the `rtp` range (`SipEndpointRtp`); `SipEndpointNotHosted` - carries `status: "sip_not_hosted"`, a `null` `host` and `rtp`, and no - transports. Narrowing on `status === "hosted"` gives a `host` and an `rtp` - range without a cast. The method is available on team and project clients, - takes no project, and needs `sessions:read`. -- Webhooks: `KNOWN_WEBHOOK_EVENT_TYPES` and `WebhookPayloadMap` add - `bansafe.risk_changed` and `bansafe.health_changed`, with the - `BanSafeRiskChangedPayload` and `BanSafeHealthChangedPayload` types and - their `BanSafeForecast`, `BanSafeRiskFactor`, `BanSafeModelRef`, - `BanSafeHealthPenalties`, and `BanSafeHealthFinding` members. The package - root exports them, and `BanSafeRiskFactor.group` is the - `BanSafeRiskFactorGroup` union of the sixteen documented groups. - `BanSafeHealthChangedPayload.previousBand` uses `BanSafeHealthBandName | null`, - matching the five health bands accepted by `band`. -- Added `Client.operations` on organization and project clients: `list` - (filter by `projectId`, status, kind, resource, and time), `get` with an - optional server long-poll (`wait`, 0 to 30 seconds), `listTransitions`, - `cancel` (generates an `Idempotency-Key`), and `wait`, which chains - long-polls until the operation is terminal or `maxWaitMs` ends. Requires - `operations:read`; `cancel` requires `operations:cancel`. -- Client rules: `ClientRules` and `SetClientRulesRequest` add - `conversationTtlSeconds`, the seconds a sender stays replyable in - `conversation` mode (300 to 604800; the API default is 86400). - `ClientRules.recipientMode` is now `conversation`, `any`, `none`, or the - legacy `verified` that older saved rules may return, instead of allowing an - empty string. `rateLimit` and `maxDaily` must be 0 or more. -- `PolymorfaErrorCode` adds `feature_unavailable`, - `stream_connection_limit_reached`, `stream_cursor_expired`, and - `stream_cursor_invalid`. -- Added `Client.events.stream()`, an async iterator over a project's - server-sent event stream with type filters, resume from a cursor, automatic - reconnect with backoff, heartbeat supervision, and retention-gap reporting. - `Client.events.acknowledgeStream()` confirms progress on manual-ack streams, - and `Client.events.liveSource()` adapts the stream for `@polymorfa/store`. - The API route is a beta that requires `events:listen` and team enrollment. -- `CreateProjectRequest.defaultTier` is typed as `"free" | "standard" | "pro"`, - and `ProductionEnrollmentResult` includes `billingMode: "payg"`. -- Drop-in components. `createPolymorfaHandler()` in `@polymorfa/nextjs` - serves the token, webhook, media, history, events, connect and template - routes from one catch-all route, with `toExpress()` and `toHono()` - adapters. Its required `mint()` callback returns an explicit grant - (session or Customer, conversations, `allow`, TTL); nothing is granted by - default. `` fetches and refreshes client - tokens with backoff and exposes `usePolymorfaClient()` and - `usePermissions()`. New React components: ``, - ``, ``, `` - (opens the hosted QuickLink page), ``, `` and - ``. Controls the token lacks are hidden, with one - development warning. `@polymorfa/elements` adds `definePolymorfa()`, - ``, `` and ``. - `@polymorfa/store` adds `createStoreInboxSource()`. `ComposeBox` gains an - `attachments` prop. See `examples/five-minute-inbox`. -- Message writes are safe to retry. `messages.send`, `messages.react`, - `chats.editMessage`, `chats.deleteMessage`, `channels.reactToMessage`, and - Messaging `campaigns.create` and `campaigns.launch` generate an - `Idempotency-Key` when you don't pass `idempotencyKey`, and their automatic - retries reuse it. The browser client's `messages.send` and `react` do the - same. A response with `Idempotent-Replayed: true` is final and is not - retried, in both the server and browser transports. A caller-supplied key - still wins. `PolymorfaErrorCode` adds `idempotency_completed`, - `idempotency_conflict`, `idempotency_in_progress`, and - `idempotency_outcome_unknown`. -- `CreateProjectRequest.icon` is typed as `ProjectIconInput`, whose `type` is - `emoji`, `icon`, or `image`, matching what the API accepts. -- Dev prereleases of `@polymorfa/sdk`, `@polymorfa/browser`, `@polymorfa/ui`, - `@polymorfa/elements`, `@polymorfa/react`, `@polymorfa/store`, - `@polymorfa/nextjs` and `@polymorfa/devtools` publish to npm under the - `dev` dist-tag on each push to `dev`. Versions follow - `0.1.0-dev.`, internal dependencies are pinned to the same - version, and each release carries npm provenance. Install with - `npm install @polymorfa/sdk@dev`. See [releasing](docs/releasing.md). -- Added `MessagingClient.testing.triggerEvent()` and - `MessagingClient.testing.listEventFixtures()` to fire signed test events for - Test numbers, with typed fixture names (`TestEventFixture`, - `TEST_EVENT_FIXTURES`), per-fixture `TestEventOverrides`, and the - `fromSession` simulated-message option. `triggerEvent()` accepts an optional - `idempotencyKey` so a retried trigger reuses the same event. -- `PolymorfaError` exposes `requestLogUrl`, `docUrl`, and `rateLimitReason`, - and `requestId` now prefers the error body's `request_id` over the - `X-Request-Id` header. `code` is typed as `PolymorfaErrorCode`, a union of - the documented codes (including the new WhatsApp codes - `recipient_not_on_whatsapp`, `conversation_window_closed`, - `template_not_approved`, `media_too_large`, `whatsapp_rate_limited`, - `new_chat_limit_reached`, and `whatsapp_account_restricted`; the Calls and SIP trunk codes; the Platform `payg_required` code; and the legacy `premium_required` code) that still - accepts any string. A `413` now throws `PolymorfaValidationError`. - `BrowserError` gains `docUrl` and reads `code`, `requestId`, and the message - from the error object. The `polymorfa-ratelimit-reason` header is kept in - response metadata. -- Breaking (types only): `Client.projects.create` returns `CreatedProject`, and - `CreateProjectRequest.defaultTier` is `ProjectDefaultTier`. - `ProductionEnrollmentResult` adds `billingMode: "payg"`. -- `MessagingClient.clientTokens.mint` accepts `customer` (a Polymorfa - Customer ID) instead of `session`, with an optional `allow` list, to mint a - Customer-scoped client token (beta). The token covers the numbers the - Customer owns when it is minted; the API re-checks ownership on every - request. `MintClientTokenRequest` is now a union of - `MintSessionClientTokenRequest` and `MintCustomerClientTokenRequest`, and - `CustomerClientTokenAction` lists the allowed actions. The SDK throws - `PolymorfaConfigurationError` before sending when both or neither of - `session` and `customer` are set, or when `allow` is set without - `customer`. `@polymorfa/nextjs` `createMessagingClientTokenMint` accepts - the same `customer` and `allow` from `resolve`. -- New opt-in package `@polymorfa/store` keeps a local IndexedDB copy of - webhook-shaped events. `createPolymorfaStore()` files messages, - conversations, contacts, presence, calls, labels, sessions, and templates - into separate stores, logs every event, and keeps other types in `custom`. - Ingest skips repeated event IDs and never replaces newer state with older - state. `connectEventSource()` follows `fromEventSource()`, - `fromEventStream()`, `fromWebSocket()`, or `fromIterable()` sources and - saves a resume cursor. `fromEventStream({ format: "project" })` reads the - project event stream frames through a backend relay. - `createStoreConversationSource()` backs - `ConversationController`, and `@polymorfa/store/react` adds - `usePolymorfaStoreQuery()`. Stores sync across tabs, apply retention, fall - back to memory when IndexedDB is unavailable, and accept `encrypt`, - `decrypt`, and `redact` options. Message content is stored on the device. - The Polymorfa client-token event stream is planned and not available yet. - See the [store guide](packages/store/README.md). -- Added streaming media downloads to `MessagingClient.media`. - `downloadStream()` returns an unbuffered body with `contentType`, - `contentLength`, `filename` and `requestId`. `downloadBlob()` returns a - typed `Blob`. `downloadUrl()` returns the short-lived signed storage URL - without following it. When the SDK follows a storage redirect, it never - sends the Polymorfa credential to the storage host. `download()` keeps its - existing behavior. -- Added direct WhatsApp media downloads. - `MessagingClient.media.downloadFromWhatsApp()` and - `downloadWhatsAppMedia()` fetch the encrypted file named by a message - webhook's `media` field from `*.whatsapp.net`, then verify and decrypt it - locally. `decodeWhatsAppMedia()`, `deriveWhatsAppMediaKeys()` and - `decryptWhatsAppMedia()` are exported for custom fetching. Integrity and - size failures raise the new `PolymorfaMediaIntegrityError`. -- Added the `@polymorfa/sdk/node` entry point. It provides - `downloadMediaToFile()`, `downloadWhatsAppMediaToFile()`, - `writeStreamToFile()` (temporary file, then rename) and `nodeMediaCrypto` - for streaming decryption. -- `@polymorfa/nextjs` adds `createMediaDownloadRoute()` with `redirect`, - `proxy` and `whatsapp` modes, a required fail-closed `authorize` callback, - and safe response headers. -- Packaging: `@polymorfa/browser` depends on `@polymorfa/sdk` and uses its - Calls client (`@polymorfa/sdk/calls`) instead of a separate package, so - there is one copy of the Calls classes: `instanceof CallsDisabledError` - (and the other Calls errors) with the class from `@polymorfa/sdk/calls` - works for errors raised by browser calls. The SDK now lives in the - `packages/typescript` workspace; installing from the GitHub repository root - (`npm install github:polymorfa/sdks#dev`) no longer installs it. Install the - packed `@polymorfa/sdk` tarball instead. -- `@polymorfa/sdk/calls`: a placed call records the answer in `call.claim` - (`answered`, `answeredBy`, `exclusive`) and emits `claim` when the callee - picks up. `disconnect()` and `leave()` wait for an answer still in flight - and leave the call if it succeeds. Concurrent token requests share one - provider call without sharing cancellation: cancelling one request no - longer fails the others, and the provider call is cancelled only when - every waiting request is. -- Call diagnostics. `MessagingClient.voip.report()` sends quality figures or - an error code for one of your media connections - (`POST /messaging/voip/calls/{id}/reports`), with the new - `VoipCallReportRequest` types. `createBrowserCalls` reports each - connection's round-trip time, jitter, packet counts, codecs, ICE candidate - type and reconnect count every 15 seconds and when it closes, plus error - codes for denied or missing devices, ICE and negotiation failures, stalled - media, reconnection give-up and unsupported browsers. `@polymorfa/sdk/calls` - reports media timeouts, token failures and reconnection give-up, and each - connection's reconnect count. Reports carry no personal data, are - best-effort, and never affect the call. Turn them off with - `diagnostics: false` on `createBrowserCalls`, the `CallsController` options - or `CallsClient`. -- The programmatic Calls client ships inside `@polymorfa/sdk` as the - `@polymorfa/sdk/calls` subpath. Import `CallsClient` and the Calls error - classes from `@polymorfa/sdk/calls`; there is no separate `@polymorfa/calls` - package to install. -- `@polymorfa/sdk/calls` on Node.js 22: a lifecycle or media socket whose - handshake fails (connection refused or reset, or a non-WebSocket reply) now - settles like a dropped socket. `connect()` resolves and retries with backoff, - and a media `connect()` rejects, instead of waiting indefinitely. -- Breaking: the `includeSelfAudio` call setting is replaced by - `conferenceMode` (default `true`) in `SessionCallSettings`, - `UpdateSessionCallSettingsRequest` and `MessagingClient.voip` - `retrieveCallSettings` / `updateCallSettings`. With conference mode on, - every participant you connect to a call (browser, app and server - connections, and SIP trunk callers) hears the WhatsApp party and each - other; with it off, each hears only the WhatsApp party. The WhatsApp party - always hears all of your participants, and nobody hears their own audio in - either mode. The platform rejects `includeSelfAudio`, and the SDK fails - before sending it with a `PolymorfaValidationError` that names - `conferenceMode`. -- `Client.sipTrunks` manages SIP trunks (a standard Calls feature; no - enrollment). Session call settings add - `callsEnabled` (turn calling off for a session; refusals use - `calls_disabled`, raised as `CallsDisabledError` by `@polymorfa/sdk/calls` and - the browser client), `inboundRoute`, `sipTrunkId`, `sipClaim`, `hostCloudApiCalls` (whether - Polymorfa Calls answers a Cloud API session's calls), and `revision`. An update changes - only the settings you send (`conferenceMode` is optional) and accepts - `expectedRevision`. Webhook types add `call.connection_joined` and - `call.connection_left`, including the SIP trunk departure reasons. -- Call webhook payload types match the contract. `CallReceivedPayload` adds - `hasVideo`, and optional `sessionConnection` and `capabilities` - (`WebhookCallCapabilities`). `CallAcceptedPayload` is its own type with - optional `answeredBy`, `exclusive`, `sessionConnection` and `capabilities`. - `CallEndedPayload` adds optional `sessionConnection`. `CallMissedPayload` and - `CallRejectedPayload` no longer extend or alias `CallReceivedPayload`. -- `@polymorfa/sdk/calls`: when media fails after an answer, join or remote - pickup, or is lost for good, the call is released on the platform before it - ends locally: ended when this client claimed it (exclusive answer or - placement), otherwise left. `answer()` and `join()` reject after the - release, which waits at most 5 seconds. -- `Call.leave()` ends a placed call that is still ringing, and a failed leave - request keeps the call live so it can be retried. A placed call's - controller status stays `ringing` until the callee answers, for every - backend, and the React stage shows "Ringing" only in that state. A partial - capability report changes only the flags it reports, and a placed call the - answer reports as audio-only opens no camera. -- Capabilities reported with `call.accepted` now apply: `Call.capabilities` - and `Call.hasVideo` update (new `capabilities` event on `Call`), and the - browser controller refreshes `snapshot.capabilities` for the displayed call - and for waiting invitations. `IncomingCallRelay.accepted()` accepts the - webhook's `capabilities`. -- Browser calls: when a microphone or camera switch fails, - `snapshot.selectedDevices` goes back to the device the capture is using, - also after several quick switches. -- Browser calls: when media fails on a call this browser claimed (an - exclusive answer or placement), the call is ended rather than left, so the - other party is not left on an answered call. The React incoming card - applies its pre-answer microphone and camera choices only to the call that - was answered. -- Browser calls: a group video slot request whose re-offer failed is offered - again on the next report, so participants beyond the initial slots become - visible; participants and an answer reported before an outbound placement - returned are shown on the placed call. With `createBrowserCalls`, a placed - call stays `ringing` while its media opens until the callee answers, then - moves to `connecting`; the React stage shows "Ringing" only until then. -- `CallsClient` no longer emits `incoming` for a call whose `call.ended`, - `call.missed` or `call.rejected` event arrived before `call.received`; the - call is reported through `ended` only. The browser `CallsController` also - ignores an invitation that arrives after its call was reported ended. -- `ClientTokenManager` (browser) and `CallsTokenSource` (`@polymorfa/sdk/calls`) - never reuse or cache a provider call that started before a forced refresh - or `invalidate()`, so a reconnect after a revoked token asks for a new one. -- Breaking: Calls use one neutral calling API. Session answer modes and - calling tickets are gone; the token your server issues authenticates - everything, and group audio and per-participant video are supported. - Upgrade steps: - - Mint browser tokens on your server with `POST /platform/client-tokens` - (`clientTokens.mint`, or `@polymorfa/nextjs` helpers). `MessagingClient.voip` - no longer has `token()`, `socketTicket()` or `agentToken()`; it has - `place`, `accept`, `reject`, `leave`, `end`, `addParticipant`, - `retrieveCallSettings` and `updateCallSettings` (`conferenceMode`, - `updatedAt` is `null` until changed). `reject` and `leave` accept - `participant` for server credentials. - - Signaling, media negotiation, sockets and media framing are internal. The - package entry points no longer export `HttpCallsApi`, `LifecycleSocket`, - `MediaSocket`, frame encoders and parsers, `VideoCodec`, - `CallsSignalingClient`, `BrowserCallsApi`, `createSignalingCallsBackend`, - `CallsSocket`, `IncomingCallRelay`, `incomingCallFromWebhook`, - `WebRtcMediaFactory`, or SDP, candidate and ticket types. Use - `CallsClient` on servers and `createBrowserCalls` in browsers. - `CallsController` is exported as a type; create it with - `createBrowserCalls`. `BrowserCallsOptions` no longer takes `media` or - `mediaFactory`, and `CallsClientOptions` no longer takes `api` or - `mediaMode`. - - `@polymorfa/sdk/calls`: pass `token` (string or provider) and, for server - credentials, `participant`. `Call.hangup()` is now `Call.end()` (ends the - call for everyone). New: `answer({ exclusive })`, `join()`, `leave()`, - `claim`, `claimedByOther`, `canJoin`, `connectionId`, the `claim` event, - the `reconnecting` state, `CallsAuthError`, and `CallClaimedError`. - `call.video` is always present; use `call.hasVideo` for the offer. - Received frames are `CallVideoFrame` (`source`, `keyframe`, - `timestampUs`, `data`); `write()` takes `OutgoingVideoFrame`; - `video.sources` maps `CallVideoSource` (`id`, `label`, `participant` or - `connectionId` and `connectionParticipant`). - - Calls report their own `capabilities` (`video`, `invite`; the browser - snapshot adds `mute`). `CallLine`, `capabilitiesFor`, the `line` options - and the React `DialPad` `line` prop are removed; use - `DialPad allowVideo={false}` for numbers whose calls cannot carry video. - `Call.capabilities` is new in `@polymorfa/sdk/calls`. - - `@polymorfa/browser`: incoming calls are never declined for you. - `CallsController` tracks every invitation (`snapshot.invitations`) and - adds `join()`, `leave()`, `end()`, `dismiss()`, `select()`, - `answer({ exclusive, callId })`, and snapshot fields `claimedByOther`, - `canJoin`, `answeredBy`, `exclusive`, `participants` and `remoteVideos`. - `controller.remoteVideos` holds a `ParticipantVideo` (`key`, `label`, - owner, `MediaStream`) per remote participant, and `remoteStream` carries - the merged call audio. An expired or revoked token reaches `onError` as - `unauthorized` and is replaced on reconnect. - - `@polymorfa/react`: new `ParticipantVideoGrid` and `ParticipantList`; - `CallSurface` and `IncomingCallCard` take `exclusive` (default `false`); - the card shows Join or Dismiss for calls answered elsewhere; `CallControls` - adds Leave (`showLeave`). `renderMedia` also receives `videos`, and - `labelVideo` receives `RemoteVideoInfo` with a `label`. - - `@polymorfa/elements`: `pmfa-call` accepts the `exclusive` attribute and - renders Join, Dismiss, Leave, waiting calls and participants. - - `CallsSnapshot.answering` is `true` while an answer or join is in flight; - the controller then refuses `answer()`, `join()`, `reject()` and - `place()`, and the React and element incoming controls are disabled. An - answer completes for the call it started on, even if another invitation - was selected meanwhile; a call dismissed during its answer is left, or - ended when the answer claimed it. The React card also disables its - pre-answer camera and microphone choices while answering. - - A refused answer keeps the call displayed as `incoming` with - `snapshot.error`. When media fails after answering, the next waiting - call is displayed and keeps the failure; `snapshot.error.callId` names - the failed call. The React card and `pmfa-call` (`failure` part) show a - notice (`calls.answerFailed`, `calls.previousFailed`). - - A call you placed offers only Hang up until it connects; - `controller.leave()` ends it in that state, and a local media failure - (such as a denied microphone) while it rings ends it rather than leaving - the callee ringing. A second `answer()` or `join()` while one is in - progress settles with the first, once media connects or fails. -- `ComposeBox` and `pmfa-compose-box` gain a WhatsApp-style toolbar: an emoji - picker with search, recent emoji, and categories; voice notes with a - recording bar, timer, and level meter; and "/" quick replies with - keyboard selection. New props are `placeholder`, `startActions`, - `endActions`, `emoji`, `voiceNotes`, `voiceNoteAutoSend`, `quickReplies`, - `onQuickReply`, and `maxRows`; the element takes the matching attributes, - a `quickReplies` property, `start-actions` and `end-actions` slots, and a - `pmfa-quick-reply` event. New slots are `composerToolbar`, `emojiButton`, - `emojiPicker`, `voiceButton`, `recordingBar`, and `quickReplyMenu`. See the - [React](packages/react/README.md#chat) and - [Web Component](packages/elements/README.md#chat) guides. -- `@polymorfa/browser` adds `VoiceNoteRecorder`, which records WebM, Ogg, or - (in Safari) MP4 audio, and `@polymorfa/ui` adds - `QuickReplyOption`, the built-in emoji set, and composer helpers. See - [voice notes](packages/browser/README.md#voice-notes). -- The `@polymorfa/devtools` panel now starts collapsed behind a small - launcher in the bottom-left corner, so it no longer covers the composer. - `mountDevAssistant()` accepts `position`, `defaultOpen`, and `offset`, and - the panel closes on Escape and follows the system color scheme. See the - [devtools guide](packages/devtools/README.md). - -- The chat and template components in `@polymorfa/react` and - `@polymorfa/elements` now ship a stylesheet: message bubbles by direction - with time and delivery status, a drawer that is full width on small - screens, a labelled template form that stacks on narrow containers, and a - WhatsApp-style template preview. Both packages follow the `light`, `dark`, - and `system` themes. Messages render oldest first whatever order the - controller holds, and the list stays at the newest message while the reader - is at the end. `ChatDrawer` shows its close button only when `onClose` is - set, and closes on Escape. `@polymorfa/ui` exports `COMPONENT_STYLES`, - `injectComponentStyles()` and `themeClassName()`, plus new locale keys for - the labels. -- Chat components in `@polymorfa/react` and `@polymorfa/elements` gain - attachments, replies, and retry. Messages show image thumbnails and file - cards, a quote of the message they reply to, date separators, grouping by - side, and pending, sent, or failed icons; failed outbound messages offer - Retry. The composer attaches files from a button, a paste, or a drop, shows - upload progress with a remove button, reports rejected files, and shows a - reply banner. `ChatDrawer` and `pmfa-chat-drawer` accept - `composerController` to render the composer and wire Reply to it, move - focus into the drawer on open, and restore it on close. The message list is - now a `role="log"` region. See the [React](packages/react/README.md#chat) - and [Web Component](packages/elements/README.md#chat) guides. -- `@polymorfa/browser` adds `LocalAttachment.file`, - `MessageAttachment.url` and `previewUrl`, `localAttachmentFromFile()`, and - `createConversationComposerActions()`, which sends composer drafts through - a `ConversationController`. `ConversationController.send()` accepts an - optional `AbortSignal`; the composer actions pass the composer's signal, so - `cancelSend()` cancels the request and marks the message failed. -- Attachment URLs render as links or images only when they use `https:`, - `http:`, or `blob:`. `@polymorfa/ui` exports `safeAttachmentUrl()` for the - same check in custom renderers. -- `ChatDrawer` and `pmfa-chat-drawer` no longer take focus when they mount - open; pass `autoFocus` (React) or set the `autofocus` attribute. Focus - still moves in when `open` changes to `true`. Escape closes a drawer only - when pressed inside it and not during IME composition. A rejected-file - message in the composer clears after an edit, a successful send, or a - reset. `pmfa-compose-box` re-renders its reply banner when its - `conversation` updates. -- `pmfa-template-builder` names its preview area `preview-panel`; `preview` - stays on the Preview button, and `slotPartName("preview")` returns - `preview-panel`. Validation messages keep their nodes between edits. -- Styling: bundled rules sit in `@layer polymorfa`, so unlayered app CSS - overrides them. Appearance adds `darkVariables` (emitted as - `--pmfa-dark-color-*`) and `unstyled`. Every chat and template node has a - slot name: React applies `appearance.elements[slot]`, a new `classNames` - prop, and `data-slot`; Web Components add kebab-case `part` names next to - the existing ones and accept `configuration.stylesheet`. `MessageList` - adds `renderAttachment` and `onReply`. See the - [styling reference](packages/ui/README.md#styling). -- Breaking: the React `MessageList` root is now a `div` with `role="log"` - that wraps the `ol`; select it with `[data-pmfa="message-list"]`. The Web - Component list part `message-list` moved to the same wrapper. Web - Components now share one adopted stylesheet instead of a `