From 69e680ee37fbb86f50a86294cb17f63c78cd729c Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 26 Sep 2026 23:09:36 +0200 Subject: [PATCH 1/3] feat(sdk): expose hosted Cloud message media download --- contracts/README.md | 8 ++++ contracts/cloud-api-supplements.json | 9 ++++ packages/typescript/README.md | 14 ++++-- packages/typescript/src/index.ts | 1 + packages/typescript/src/messaging/chats.ts | 46 ++++++++++++++++++++ packages/typescript/src/messaging/history.ts | 17 +++++++- 6 files changed, 91 insertions(+), 4 deletions(-) diff --git a/contracts/README.md b/contracts/README.md index da37634b..cd4b6c42 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -78,6 +78,14 @@ its WABA and catalog path, `sessions:read` credential boundary, projected product fields, and opaque cursor response. This does not change the pinned full snapshot or establish live catalog access. +API PR #333 at `5044826d122759aaa8be3e2c2d0f120a326fda81` adds the +Official API message-media retrieval status and stored-copy download route. +`cloud-api-supplements.json` records its exact Messaging OpenAPI hash. +`MessagingClient.chats` now exposes the typed retrieval status and buffered or +streamed message-scoped download. API PR #385 adds a final authority recheck +before history metadata responses; it changes no OpenAPI shape. These branches +do not establish deployed HMS beta access or SDK package publication. + The merged Campaigns failed-state follow-up updates the `campaign.failed` reason example and the public archive/delete `409` descriptions. It changes no operation fingerprint or SDK method mapping. diff --git a/contracts/cloud-api-supplements.json b/contracts/cloud-api-supplements.json index fe1570e1..68e0eda4 100644 --- a/contracts/cloud-api-supplements.json +++ b/contracts/cloud-api-supplements.json @@ -153,6 +153,15 @@ "sha256": "1270c8fb32a09cb717f3ea8edd6bd657e8ff3110e03a6286e68d5e16affff474", "operationIds": ["graphGetMarketingStatus"], "merged": false + }, + { + "pullRequest": "https://github.com/polymorfa/polymorfa/pull/333", + "commit": "5044826d122759aaa8be3e2c2d0f120a326fda81", + "sourcePath": "apps/api/docs/openapi.json", + "sha256": "b7b79f399ce9f6944497a2970e184fe40c276dff9034726297ca18b2aab0bd41", + "operationIds": ["getChatMessageMedia"], + "schemas": ["HistoryMediaRetrieval", "HistoryMessage"], + "merged": false } ] } diff --git a/packages/typescript/README.md b/packages/typescript/README.md index c6c1391a..40dbe589 100644 --- a/packages/typescript/README.md +++ b/packages/typescript/README.md @@ -1143,14 +1143,22 @@ if (page.data.nextCursor) { console.log(page.metadata.headers["polymorfa-data-region"]); ``` -These four reads require an organization key or project token, a visible +These conversation and message reads require an organization key or project token, a visible Number with hosted message storage enabled, team enrollment in `messaging.history`, and `chats:read` or `messages:read` as appropriate. The feature is an unreleased enrolled beta; an SDK method does not grant access. Client tokens are refused before transport. A disabled HMS Number yields `404 hms_not_enabled`; absent beta access yields `403 permission_denied`, and -an unavailable regional read yields `503 service_unavailable`. Media entries -carry an API download path, not a signed URL; downloading requires `media:read`. +an unavailable regional read yields `503 service_unavailable`. + +For Official API media, `message.mediaRetrieval.state` reports whether a copy +is `pending`, `stored`, or in a final state without a copy. A `stored` message +has a media entry whose `url` is a message-scoped API path, not a signed URL. +Download that copy with `messaging.chats.downloadMessageMedia(session, conversation, message.id)` +or stream it with `downloadMessageMediaStream(session, conversation, message.id)`. These reads +require both `messages:read` and `media:read`; a missing copy returns 404. +`downloadMessageMedia` buffers the whole file, so prefer the stream for large +media. Linked-device media paths still use `messaging.media.download(mediaId)`. Client tokens can call all five Messages operations only when the corresponding live rule is enabled: `send_message` for send and star, `send_reaction` for diff --git a/packages/typescript/src/index.ts b/packages/typescript/src/index.ts index ddb473c5..3b287ccc 100644 --- a/packages/typescript/src/index.ts +++ b/packages/typescript/src/index.ts @@ -100,6 +100,7 @@ export type { DeleteMessageOptions } from "./messaging/chats.js"; export type { HistoryChat, HistoryMedia, + HistoryMediaRetrieval, HistoryMessage, HistoryMessageSummary, HistoryPage, diff --git a/packages/typescript/src/messaging/chats.ts b/packages/typescript/src/messaging/chats.ts index 00c64db1..66bc2eb5 100644 --- a/packages/typescript/src/messaging/chats.ts +++ b/packages/typescript/src/messaging/chats.ts @@ -11,6 +11,7 @@ import type { ListHistoryChatsParams, ListHistoryMessagesParams, } from "./history.js"; +import type { MediaDownloadStream } from "./media.js"; import type { DisappearingTimerRequest, EditMessageRequest, @@ -115,6 +116,51 @@ export class ChatsResource { }); } + /** Downloads a stored Official API message copy. Requires `messages:read`, `media:read`, and enrolled HMS history access. */ + downloadMessageMedia( + session: string, + conversation: string, + messageId: string, + options: RequestOptions = {}, + ): Promise> { + this.assertHistoryCredential(); + return this.transport.requestBinary({ + method: "GET", + path: `${chatMessagePath(session, conversation, messageId)}/media`, + ...options, + }); + } + + /** Streams a stored Official API message copy without buffering the file. */ + async downloadMessageMediaStream( + session: string, + conversation: string, + messageId: string, + options: RequestOptions = {}, + ): Promise { + this.assertHistoryCredential(); + const response = await this.transport.requestStream({ + method: "GET", + path: `${chatMessagePath(session, conversation, messageId)}/media`, + ...options, + }); + return Object.freeze({ + body: response.body, + ...(response.contentType === undefined + ? {} + : { contentType: response.contentType }), + ...(response.contentLength === undefined + ? {} + : { contentLength: response.contentLength }), + ...(response.filename === undefined ? {} : { filename: response.filename }), + ...(response.metadata.requestId === undefined + ? {} + : { requestId: response.metadata.requestId }), + redirected: response.redirected, + metadata: response.metadata, + }); + } + editMessage( session: string, chatId: string, diff --git a/packages/typescript/src/messaging/history.ts b/packages/typescript/src/messaging/history.ts index 4f67bc5e..133fd94e 100644 --- a/packages/typescript/src/messaging/history.ts +++ b/packages/typescript/src/messaging/history.ts @@ -22,10 +22,24 @@ export interface HistoryMedia { readonly id: string; readonly mimeType: string; readonly fileLength: number; - /** API path for `MessagingClient.media.download`; this is not a signed URL. */ + /** API download path. Cloud stored copies use `MessagingClient.chats.downloadMessageMedia`. */ readonly url: string; } +/** Official API media copy status. Other final states have no downloadable copy. */ +export interface HistoryMediaRetrieval { + readonly state: + | "pending" + | "stored" + | "unavailable" + | "expired" + | "too_large" + | "unsupported_type" + | "failed" + | "cancelled"; + readonly reason?: string; +} + export interface HistoryMessage extends HistoryMessageSummary { readonly conversation: ConversationIdentity & { readonly sender?: ConversationIdentity; @@ -51,6 +65,7 @@ export interface HistoryMessage extends HistoryMessageSummary { readonly hash: string; }[]; readonly media?: readonly HistoryMedia[]; + readonly mediaRetrieval?: HistoryMediaRetrieval; } export interface HistoryPage { From 42b2f3075a53971963e1b9b446f46d73cecd66dc Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sat, 26 Sep 2026 23:29:50 +0200 Subject: [PATCH 2/3] style(sdk): format history media exports --- packages/typescript/src/index.ts | 5 ++++- packages/typescript/src/messaging/chats.ts | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/typescript/src/index.ts b/packages/typescript/src/index.ts index 3b287ccc..7257670d 100644 --- a/packages/typescript/src/index.ts +++ b/packages/typescript/src/index.ts @@ -62,7 +62,10 @@ export { 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 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/chats.ts b/packages/typescript/src/messaging/chats.ts index 66bc2eb5..e8293693 100644 --- a/packages/typescript/src/messaging/chats.ts +++ b/packages/typescript/src/messaging/chats.ts @@ -152,7 +152,9 @@ export class ChatsResource { ...(response.contentLength === undefined ? {} : { contentLength: response.contentLength }), - ...(response.filename === undefined ? {} : { filename: response.filename }), + ...(response.filename === undefined + ? {} + : { filename: response.filename }), ...(response.metadata.requestId === undefined ? {} : { requestId: response.metadata.requestId }), From 91fe99fadea884badebc62c92e7f86c1b404ac2d Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Sun, 27 Sep 2026 00:44:15 +0200 Subject: [PATCH 3/3] Record merged OA08 API authority source --- contracts/README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/contracts/README.md b/contracts/README.md index e5b1990c..d5ef0b7d 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -94,10 +94,15 @@ full snapshot or establish live catalog access. API PR #333 at `5044826d122759aaa8be3e2c2d0f120a326fda81` adds the Official API message-media retrieval status and stored-copy download route. `cloud-api-supplements.json` records its exact Messaging OpenAPI hash. +API PR #333 merged into `dev` as `0f2f01f6726c167fe7d7cffc3cf5238d0b3b22e0`; +the history-media path and `HistoryMedia`, `HistoryMediaRetrieval`, and +`HistoryMessage` schemas are unchanged from the pinned PR head. The complete +OpenAPI file changed with unrelated API work, so full-snapshot consolidation +remains a separate publication gate. `MessagingClient.chats` now exposes the typed retrieval status and buffered or streamed message-scoped download. API PR #385 adds a final authority recheck -before history metadata responses; it changes no OpenAPI shape. These branches -do not establish deployed HMS beta access or SDK package publication. +before history metadata responses; it changes no OpenAPI shape. This merge +does not establish deployed HMS beta access or SDK package publication. The merged Campaigns failed-state follow-up updates the `campaign.failed` reason example and the public archive/delete `409` descriptions. It changes no