From 99916093dc9ab499c74889d76b2b69fb73923988 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Mon, 14 Sep 2026 03:13:37 +0300 Subject: [PATCH 1/5] Adopt public messaging identifiers and pin coordinated SDK --- README.md | 73 ++++++++++--------- package-lock.json | 7 +- package.json | 6 +- src/base-command.ts | 2 +- src/commands/auth/status.ts | 4 +- src/commands/business/catalog/view.ts | 6 +- src/commands/business/collection/list.ts | 6 +- src/commands/business/collection/view.ts | 6 +- src/commands/business/product/view.ts | 6 +- src/commands/call/reject.ts | 5 +- src/commands/channel/message/mark-viewed.ts | 8 +- src/commands/channel/message/react.ts | 10 +-- src/commands/contact/block.ts | 5 +- src/commands/contact/business-profile.ts | 5 +- src/commands/contact/devices.ts | 7 +- src/commands/contact/info.ts | 5 +- src/commands/contact/picture.ts | 5 +- src/commands/contact/unblock.ts | 5 +- src/commands/contact/view.ts | 5 +- src/commands/group/participants/list.ts | 2 +- src/commands/{lid => identity}/resolve.ts | 28 ++++--- src/commands/label/chat/list.ts | 5 +- src/commands/label/chat/replace.ts | 5 +- src/commands/message/mark-seen.ts | 8 +- src/commands/message/react.ts | 12 ++- src/commands/message/send/text.ts | 21 ++---- src/commands/message/star.ts | 12 ++- src/commands/message/typing.ts | 8 +- src/commands/operation/view.ts | 48 ------------ src/commands/presence/chat/subscribe.ts | 2 +- src/commands/presence/chat/view.ts | 2 +- src/commands/user/security-code.ts | 2 +- src/sdk/client.ts | 7 +- src/services/auth.ts | 6 +- src/services/business.ts | 2 +- src/services/campaigns.ts | 31 +------- src/services/channels.ts | 32 +++----- src/services/contacts.ts | 18 ++--- src/services/conversation.ts | 14 ++++ src/services/customers.ts | 2 +- src/services/messages.ts | 7 +- src/services/platform-management.ts | 67 ++++------------- src/services/session-identities.ts | 48 ++++++------ test/commands/api.test.ts | 44 +++++++---- test/commands/auth.test.ts | 14 ++-- test/commands/business.test.ts | 65 +++++++++-------- test/commands/calls-and-identities.test.ts | 31 ++++---- test/commands/campaigns.test.ts | 29 ++++---- test/commands/channels.test.ts | 67 +++++++++-------- test/commands/contacts.test.ts | 59 ++++++++------- test/commands/customers.test.ts | 31 ++++---- test/commands/groups.test.ts | 7 +- test/commands/labels.test.ts | 17 +++-- test/commands/media.test.ts | 11 +-- test/commands/messages.test.ts | 81 +++++++++++---------- test/commands/platform-management.test.ts | 66 ++++++++++------- test/commands/presence.test.ts | 11 +-- test/commands/privacy.test.ts | 9 ++- test/commands/profile.test.ts | 15 ++-- test/commands/quick-replies.test.ts | 11 +-- test/context/environment.test.ts | 23 ++++-- test/input/stdin.test.ts | 10 ++- test/sdk/client.test.ts | 32 ++++++-- test/services/auth.test.ts | 21 ++++-- test/services/groups.test.ts | 10 ++- 65 files changed, 643 insertions(+), 586 deletions(-) rename src/commands/{lid => identity}/resolve.ts (64%) delete mode 100644 src/commands/operation/view.ts create mode 100644 src/services/conversation.ts diff --git a/README.md b/README.md index 6c60d690..74801de6 100644 --- a/README.md +++ b/README.md @@ -117,30 +117,32 @@ Send text and perform the complete set of source-defined Messaging actions: ```shell polymorfa message send text \ --session session_123 \ - --to 15551234567@s.whatsapp.net \ + --to +15551234567 \ --body 'Hello' -polymorfa message react message_123 \ +polymorfa message react 740000000001 \ --session session_123 \ - --chat 15551234567@s.whatsapp.net \ + --chat +15551234567 \ --emoji '👍' -polymorfa message mark-seen message_123 \ +polymorfa message mark-seen 740000000001 \ --session session_123 \ - --chat 15551234567@s.whatsapp.net + --chat +15551234567 polymorfa message typing \ --session session_123 \ - --chat 15551234567@s.whatsapp.net \ + --chat +15551234567 \ --state typing -polymorfa message star message_123 \ +polymorfa message star 740000000001 \ --session session_123 \ - --chat 15551234567@s.whatsapp.net + --chat +15551234567 ``` Add `--remove` to `message star` to unstar. Text sends accept repeatable -`--mention`, paired `--reply-to` and `--reply-participant`, and `--forwarded`. +`--mention` with a Polymorfa user ID, `--reply-to` with a Polymorfa message ID, +and `--forwarded`. `--to` and `--chat` accept a conversation ID, E.164 phone +number, or BSUID. The API resolves message direction and sender identity. Every mutation accepts `--idempotency-key` for safe retry identity. Server API keys require `messages:write`; client tokens remain subject to their live recipient rules, allowed actions, and rate limits. @@ -185,10 +187,10 @@ polymorfa label create --session session_123 --name Priority --color 5 polymorfa label update label_123 --session session_123 --name Urgent polymorfa label delete label_123 --session session_123 --yes -polymorfa label chat list 15551234567@s.whatsapp.net \ +polymorfa label chat list +15551234567 \ --session session_123 -polymorfa label chat replace 15551234567@s.whatsapp.net \ +polymorfa label chat replace +15551234567 \ --session session_123 \ --label priority \ --label customer @@ -300,10 +302,10 @@ renew policy-governed chat observations: polymorfa presence view --session session_123 polymorfa presence set --session session_123 --state available -polymorfa presence chat view 15551234567@s.whatsapp.net \ +polymorfa presence chat view +15551234567 \ --session session_123 -polymorfa presence chat subscribe 15551234567@s.whatsapp.net \ +polymorfa presence chat subscribe +15551234567 \ --session session_123 ``` @@ -374,14 +376,14 @@ polymorfa contact check \ --phone +15551234567 \ --phone +15557654321 -polymorfa contact view alice@lid --session session_123 -polymorfa contact info alice@lid --session session_123 -polymorfa contact picture alice@lid --session session_123 -polymorfa contact devices alice@lid --session session_123 -polymorfa contact business-profile alice@lid --session session_123 +polymorfa contact view 750000000001 --session session_123 +polymorfa contact info 750000000001 --session session_123 +polymorfa contact picture 750000000001 --session session_123 +polymorfa contact devices 750000000001 --session session_123 +polymorfa contact business-profile 750000000001 --session session_123 polymorfa contact blocklist --session session_123 -polymorfa contact block alice@lid --session session_123 -polymorfa contact unblock alice@lid --session session_123 +polymorfa contact block 750000000001 --session session_123 +polymorfa contact unblock 750000000001 --session session_123 ``` `contact view` reads contact data known to the connected session. `contact info` @@ -403,32 +405,32 @@ source tags: ```shell polymorfa call reject call_123 \ --session session_123 \ - --from caller@lid \ + --from 750000000001 \ --idempotency-key reject_call_123 -polymorfa lid resolve \ +polymorfa identity resolve \ --session session_123 \ --phone-number +15551234567 -polymorfa lid resolve \ +polymorfa identity resolve \ --session session_123 \ --username alice \ --username-key 1234 -polymorfa user security-code 15551234567@lid --session session_123 +polymorfa user security-code 750000000001 --session session_123 ``` All three require a server API key and an active Linked Device session. Call -rejection requires the incoming call ID and caller JID; it is not call accept, -placement, history, or streaming. LID resolution accepts exactly one phone -number, stable `digits@lid` ID, deprecated `--lid` alias, or username. A username +rejection requires the incoming call ID and caller's Polymorfa ID; it is not call accept, +placement, history, or streaming. Identity resolution accepts exactly one phone +number, decimal Polymorfa user ID, or username. A username key is exactly four digits and is valid only with `--username`. -Security-code lookup accepts only a stable `digits@lid` user ID. Human output +Security-code lookup accepts only a decimal Polymorfa user ID. Human output shows the 60-digit numeric verification code and identity, but does not dump the base64 display QR. `--json` preserves the complete response and no-store header metadata. The private WhatsApp verification payload is never returned by the -API. The source exposes no bulk LID resolution, call list/history, security-code +API. The source exposes no bulk identity resolution, call list/history, security-code history/refresh, or verification-submit operation. ## Business App @@ -440,11 +442,11 @@ separate: ```shell polymorfa business profile view --session session_123 -polymorfa business catalog view --session session_123 --jid shop@lid +polymorfa business catalog view --session session_123 --id 750000000001 polymorfa business product view product_123 \ --session session_123 \ - --jid shop@lid -polymorfa business collection list --session session_123 --jid shop@lid + --id 750000000001 +polymorfa business collection list --session session_123 --id 750000000001 polymorfa business compliance view --session session_123 polymorfa business accounts view --session session_123 polymorfa business eligibility view --session session_123 @@ -506,7 +508,6 @@ polymorfa audit-log list --action key.revoked --resource api-key --limit 100 polymorfa session-ban list --active polymorfa security-incident list polymorfa security-incident acknowledge incident_123 -polymorfa operation view operation_123 polymorfa project-token list --project project_123 polymorfa widget view --project project_123 polymorfa widget update --project project_123 --input widget-patch.json @@ -523,8 +524,8 @@ this route. Security-incident acknowledgement is repeatable and sends no body. Audit filters are exact matches, and `--limit` is bounded to 1 through 500. These list operations return complete arrays and expose no cursor. Project-token -listing requires one explicit project ID. `operation view` is a single durable -state read; it does not wait, watch, stream, list, or cancel operations. +listing requires one explicit project ID. Operation inspection is console-only; +the CLI does not expose operation polling or cancellation. Member invite, role update, and removal remain dashboard-session routes and are not exposed through server-key commands. The API also exposes no compatible @@ -574,7 +575,7 @@ Launch, pause, resume, and stop return accepted durable work; they do not wait for the state transition. Poll the returned operation explicitly: ```shell -polymorfa operation view operation_123 --surface messaging + ``` Launch times accept epoch milliseconds or timezone-qualified ISO 8601 diff --git a/package-lock.json b/package-lock.json index 57cc7b69..84027828 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@napi-rs/keyring": "1.3.0", "@oclif/core": "^4.13.5", - "@polymorfa/sdk": "github:polymorfa/sdks#bf5969e68eda2c0cf5541076bd2c55eb51f09f13" + "@polymorfa/sdk": "github:polymorfa/sdks#31467bdc4e06cc962e8d06f14dd98f4f365b367b" }, "bin": { "polymorfa": "bin/run.js" @@ -972,9 +972,10 @@ }, "node_modules/@polymorfa/sdk": { "version": "0.1.0-dev.0", - "resolved": "git+ssh://git@github.com/polymorfa/sdks.git#bf5969e68eda2c0cf5541076bd2c55eb51f09f13", - "integrity": "sha512-aF4esaJcCTdJkn3p7qfKVJjEFKNNURrZl/p3ovnmWYbXxEfTu/mjDNV//resa1eYjBhiXxDYwDitPgDpAVRJhQ==", + "resolved": "git+ssh://git@github.com/polymorfa/sdks.git#31467bdc4e06cc962e8d06f14dd98f4f365b367b", + "integrity": "sha512-F7MJsEXMfxo/cyW7u1b6oVOJsG9JOIilEpgf+xVIsP35216lSmbiIOYZ6mTDWiUVukrmGm/yoQvC4mAfiMY8EA==", "workspaces": [ + "packages/calls", "packages/browser", "packages/ui", "packages/elements", diff --git a/package.json b/package.json index cbcb6beb..cbbb2040 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "dependencies": { "@napi-rs/keyring": "1.3.0", "@oclif/core": "^4.13.5", - "@polymorfa/sdk": "github:polymorfa/sdks#bf5969e68eda2c0cf5541076bd2c55eb51f09f13" + "@polymorfa/sdk": "github:polymorfa/sdks#31467bdc4e06cc962e8d06f14dd98f4f365b367b" }, "devDependencies": { "@eslint/js": "^9.39.5", @@ -133,8 +133,8 @@ "label:chat": { "description": "Manage the complete label set assigned to a chat." }, - "lid": { - "description": "Resolve WhatsApp identities to stable LIDs." + "identity": { + "description": "Resolve phone numbers and usernames to Polymorfa user IDs." }, "message": { "description": "Send messages and perform message actions." diff --git a/src/base-command.ts b/src/base-command.ts index e611e09c..cf5e601f 100644 --- a/src/base-command.ts +++ b/src/base-command.ts @@ -2,7 +2,7 @@ import {Command, Flags, type Interfaces} from '@oclif/core' import { PolymorfaConfigurationError, type MessagingClient, - type PlatformClient, + type Client as PlatformClient, } from '@polymorfa/sdk' import {resolveCommandEnvironment} from './context/environment.js' diff --git a/src/commands/auth/status.ts b/src/commands/auth/status.ts index b61a7a32..eb41aaac 100644 --- a/src/commands/auth/status.ts +++ b/src/commands/auth/status.ts @@ -3,7 +3,7 @@ import {createAuthService} from '../../config/auth-service.js' import { PolymorfaConfigurationError, validateMessagingCredential, - validatePlatformApiKey, + validateOrganizationApiKey, } from '@polymorfa/sdk' export default class AuthStatus extends BaseCommand { @@ -59,7 +59,7 @@ function environmentStatus( ) } - if (apiKey !== undefined) validatePlatformApiKey(apiKey) + if (apiKey !== undefined) validateOrganizationApiKey(apiKey) if (clientToken !== undefined) { validateMessagingCredential({type: 'clientToken', value: clientToken}) } diff --git a/src/commands/business/catalog/view.ts b/src/commands/business/catalog/view.ts index 223d91b9..5b2e4ea0 100644 --- a/src/commands/business/catalog/view.ts +++ b/src/commands/business/catalog/view.ts @@ -18,8 +18,8 @@ export default class BusinessCatalogView extends BaseCommand { max: 1024, min: 1, }), - jid: Flags.string({ - description: 'Business account user or LID JID.', + id: Flags.string({ + description: 'Polymorfa business user ID.', required: true, }), limit: Flags.integer({ @@ -44,7 +44,7 @@ export default class BusinessCatalogView extends BaseCommand { await this.messagingClient(flags, {serverApiKeyOnly: 'Business App'}), flags.session, { - jid: flags.jid, + id: flags.id, ...(flags.after === undefined ? {} : {after: flags.after}), ...(flags.limit === undefined ? {} : {limit: flags.limit}), ...(flags.width === undefined ? {} : {width: flags.width}), diff --git a/src/commands/business/collection/list.ts b/src/commands/business/collection/list.ts index 59659065..770a18e6 100644 --- a/src/commands/business/collection/list.ts +++ b/src/commands/business/collection/list.ts @@ -28,8 +28,8 @@ export default class BusinessCollectionList extends BaseCommand { max: 100, min: 1, }), - jid: Flags.string({ - description: 'Business account user or LID JID.', + id: Flags.string({ + description: 'Polymorfa business user ID.', required: true, }), session: Flags.string({ @@ -49,7 +49,7 @@ export default class BusinessCollectionList extends BaseCommand { await this.messagingClient(flags, {serverApiKeyOnly: 'Business App'}), flags.session, { - jid: flags.jid, + id: flags.id, ...(flags.after === undefined ? {} : {after: flags.after}), ...(flags['collection-limit'] === undefined ? {} diff --git a/src/commands/business/collection/view.ts b/src/commands/business/collection/view.ts index fc22504b..34872bec 100644 --- a/src/commands/business/collection/view.ts +++ b/src/commands/business/collection/view.ts @@ -26,8 +26,8 @@ export default class BusinessCollectionView extends BaseCommand { max: 1024, min: 1, }), - jid: Flags.string({ - description: 'Business account user or LID JID.', + id: Flags.string({ + description: 'Polymorfa business user ID.', required: true, }), limit: Flags.integer({ @@ -53,7 +53,7 @@ export default class BusinessCollectionView extends BaseCommand { flags.session, args.collection, { - jid: flags.jid, + id: flags.id, ...(flags.after === undefined ? {} : {after: flags.after}), ...(flags.limit === undefined ? {} : {limit: flags.limit}), ...(flags.width === undefined ? {} : {width: flags.width}), diff --git a/src/commands/business/product/view.ts b/src/commands/business/product/view.ts index bdaa84cd..f4bf73c5 100644 --- a/src/commands/business/product/view.ts +++ b/src/commands/business/product/view.ts @@ -14,8 +14,8 @@ export default class BusinessProductView extends BaseCommand { static override description = 'View one business product.' static override flags = { - jid: Flags.string({ - description: 'Business account user or LID JID.', + id: Flags.string({ + description: 'Polymorfa business user ID.', required: true, }), session: Flags.string({ @@ -30,7 +30,7 @@ export default class BusinessProductView extends BaseCommand { await this.messagingClient(flags, {serverApiKeyOnly: 'Business App'}), flags.session, args.product, - {jid: flags.jid}, + {id: flags.id}, ) this.writeValue(result, renderBusinessProduct(result.data), flags) } diff --git a/src/commands/call/reject.ts b/src/commands/call/reject.ts index 07e74fd7..9eb91e67 100644 --- a/src/commands/call/reject.ts +++ b/src/commands/call/reject.ts @@ -14,7 +14,10 @@ export default class CallReject extends BaseCommand { static override description = 'Reject an incoming Linked Device call.' static override flags = { - from: Flags.string({description: 'Incoming caller JID.', required: true}), + from: Flags.string({ + description: 'Incoming caller Polymorfa ID.', + required: true, + }), 'idempotency-key': Flags.string({ description: 'Safely identify retries of this call rejection.', }), diff --git a/src/commands/channel/message/mark-viewed.ts b/src/commands/channel/message/mark-viewed.ts index e9cccb3c..ceedb67f 100644 --- a/src/commands/channel/message/mark-viewed.ts +++ b/src/commands/channel/message/mark-viewed.ts @@ -6,8 +6,8 @@ import {markChannelMessageViewed} from '../../../services/channels.js' export default class ChannelMessageMarkViewed extends BaseCommand { static override args = { channel: Args.string({description: 'Channel ID.', required: true}), - 'server-id': Args.string({ - description: 'Positive numeric message server ID.', + 'message-id': Args.string({ + description: 'Polymorfa message ID.', required: true, }), } @@ -30,14 +30,14 @@ export default class ChannelMessageMarkViewed extends BaseCommand { await this.messagingClient(flags, {serverApiKeyOnly: 'Channel'}), flags.session, args.channel, - args['server-id'], + args['message-id'], flags['idempotency-key'] === undefined ? {} : {idempotencyKey: flags['idempotency-key']}, ) this.writeValue( result, - `Marked channel message ${args['server-id']} as viewed.`, + `Marked channel message ${args['message-id']} as viewed.`, flags, ) } diff --git a/src/commands/channel/message/react.ts b/src/commands/channel/message/react.ts index 91154caf..ed6b38d4 100644 --- a/src/commands/channel/message/react.ts +++ b/src/commands/channel/message/react.ts @@ -6,8 +6,8 @@ import {reactToChannelMessage} from '../../../services/channels.js' export default class ChannelMessageReact extends BaseCommand { static override args = { channel: Args.string({description: 'Channel ID.', required: true}), - 'server-id': Args.string({ - description: 'Positive numeric message server ID.', + 'message-id': Args.string({ + description: 'Polymorfa message ID.', required: true, }), } @@ -35,7 +35,7 @@ export default class ChannelMessageReact extends BaseCommand { await this.messagingClient(flags, {serverApiKeyOnly: 'Channel'}), flags.session, args.channel, - args['server-id'], + args['message-id'], flags.reaction, flags['idempotency-key'] === undefined ? {} @@ -44,8 +44,8 @@ export default class ChannelMessageReact extends BaseCommand { this.writeValue( result, flags.reaction.length === 0 - ? `Removed the reaction from channel message ${args['server-id']}.` - : `Reacted to channel message ${args['server-id']}.`, + ? `Removed the reaction from channel message ${args['message-id']}.` + : `Reacted to channel message ${args['message-id']}.`, flags, ) } diff --git a/src/commands/contact/block.ts b/src/commands/contact/block.ts index 9d1ecf9b..414ee0d7 100644 --- a/src/commands/contact/block.ts +++ b/src/commands/contact/block.ts @@ -5,7 +5,10 @@ import {blockContact} from '../../services/contacts.js' export default class ContactBlock extends BaseCommand { static override args = { - contact: Args.string({description: 'Contact LID or JID.', required: true}), + contact: Args.string({ + description: 'Polymorfa contact ID.', + required: true, + }), } static override description = 'Block a contact.' diff --git a/src/commands/contact/business-profile.ts b/src/commands/contact/business-profile.ts index e31d3f81..897539e8 100644 --- a/src/commands/contact/business-profile.ts +++ b/src/commands/contact/business-profile.ts @@ -8,7 +8,10 @@ import { export default class ContactBusinessProfile extends BaseCommand { static override args = { - contact: Args.string({description: 'Contact LID or JID.', required: true}), + contact: Args.string({ + description: 'Polymorfa contact ID.', + required: true, + }), } static override description = 'Get a contact’s WhatsApp business profile.' diff --git a/src/commands/contact/devices.ts b/src/commands/contact/devices.ts index 3bb1a182..39d7f063 100644 --- a/src/commands/contact/devices.ts +++ b/src/commands/contact/devices.ts @@ -5,10 +5,13 @@ import {listContactDevices} from '../../services/contacts.js' export default class ContactDevices extends BaseCommand { static override args = { - contact: Args.string({description: 'Contact LID or JID.', required: true}), + contact: Args.string({ + description: 'Polymorfa contact ID.', + required: true, + }), } - static override description = 'List a contact’s linked-device JIDs.' + static override description = 'List a contact’s linked devices.' static override flags = { session: Flags.string({ diff --git a/src/commands/contact/info.ts b/src/commands/contact/info.ts index c7e12d85..6fcb9f2c 100644 --- a/src/commands/contact/info.ts +++ b/src/commands/contact/info.ts @@ -5,7 +5,10 @@ import {renderContactInfo, viewContactInfo} from '../../services/contacts.js' export default class ContactInfo extends BaseCommand { static override args = { - contact: Args.string({description: 'Contact LID or JID.', required: true}), + contact: Args.string({ + description: 'Polymorfa contact ID.', + required: true, + }), } static override description = 'Get live WhatsApp metadata for a contact.' diff --git a/src/commands/contact/picture.ts b/src/commands/contact/picture.ts index 994fbc8c..b4c7d862 100644 --- a/src/commands/contact/picture.ts +++ b/src/commands/contact/picture.ts @@ -5,7 +5,10 @@ import {viewContactPicture} from '../../services/contacts.js' export default class ContactPicture extends BaseCommand { static override args = { - contact: Args.string({description: 'Contact LID or JID.', required: true}), + contact: Args.string({ + description: 'Polymorfa contact ID.', + required: true, + }), } static override description = 'Get a contact profile-picture URL.' diff --git a/src/commands/contact/unblock.ts b/src/commands/contact/unblock.ts index 03e1fa5a..34240e46 100644 --- a/src/commands/contact/unblock.ts +++ b/src/commands/contact/unblock.ts @@ -5,7 +5,10 @@ import {unblockContact} from '../../services/contacts.js' export default class ContactUnblock extends BaseCommand { static override args = { - contact: Args.string({description: 'Contact LID or JID.', required: true}), + contact: Args.string({ + description: 'Polymorfa contact ID.', + required: true, + }), } static override description = 'Unblock a contact.' diff --git a/src/commands/contact/view.ts b/src/commands/contact/view.ts index 74f1ff08..614772ef 100644 --- a/src/commands/contact/view.ts +++ b/src/commands/contact/view.ts @@ -5,7 +5,10 @@ import {renderContact, viewContact} from '../../services/contacts.js' export default class ContactView extends BaseCommand { static override args = { - contact: Args.string({description: 'Contact LID.', required: true}), + contact: Args.string({ + description: 'Polymorfa contact ID.', + required: true, + }), } static override description = 'View stored contact data known to a session.' diff --git a/src/commands/group/participants/list.ts b/src/commands/group/participants/list.ts index cb5c2f57..011c995d 100644 --- a/src/commands/group/participants/list.ts +++ b/src/commands/group/participants/list.ts @@ -34,7 +34,7 @@ export default class GroupParticipantsList extends BaseCommand { : participant.isAdmin ? 'admin' : 'member' - return `${participant.lid}\t${participant.phoneNumber ?? '-'}\t${role}` + return `${participant.id}\t${participant.phoneNumber ?? '-'}\t${role}` }) .join('\n') this.writeValue(result, human, flags) diff --git a/src/commands/lid/resolve.ts b/src/commands/identity/resolve.ts similarity index 64% rename from src/commands/lid/resolve.ts rename to src/commands/identity/resolve.ts index 2eef0890..007eb784 100644 --- a/src/commands/lid/resolve.ts +++ b/src/commands/identity/resolve.ts @@ -2,24 +2,21 @@ import {Flags} from '@oclif/core' import {BaseCommand} from '../../base-command.js' import { - renderResolvedLid, - resolveLid, + renderResolvedIdentity, + resolveIdentity, } from '../../services/session-identities.js' -const IDENTITY_FLAGS = ['phone-number', 'id', 'lid', 'username'] +const IDENTITY_FLAGS = ['phone-number', 'id', 'username'] -export default class LidResolve extends BaseCommand { - static override description = 'Resolve one phone number, LID, or username.' +export default class IdentityResolve extends BaseCommand { + static override description = + 'Resolve one phone number, user ID, or username.' static override flags = { id: Flags.string({ - description: 'Stable user ID in digits@lid form.', + description: 'Opaque Polymorfa user ID.', exclusive: IDENTITY_FLAGS.filter((name) => name !== 'id'), }), - lid: Flags.string({ - description: 'Deprecated alias for --id.', - exclusive: IDENTITY_FLAGS.filter((name) => name !== 'lid'), - }), 'phone-number': Flags.string({ description: 'Phone digits with an optional leading plus sign.', exclusive: IDENTITY_FLAGS.filter((name) => name !== 'phone-number'), @@ -39,13 +36,14 @@ export default class LidResolve extends BaseCommand { } async run(): Promise { - const {flags} = await this.parse(LidResolve) - const result = await resolveLid( - await this.messagingClient(flags, {serverApiKeyOnly: 'LID'}), + const {flags} = await this.parse(IdentityResolve) + const result = await resolveIdentity( + await this.messagingClient(flags, { + serverApiKeyOnly: 'identity resolution', + }), flags.session, { ...(flags.id === undefined ? {} : {id: flags.id}), - ...(flags.lid === undefined ? {} : {lid: flags.lid}), ...(flags['phone-number'] === undefined ? {} : {phoneNumber: flags['phone-number']}), @@ -55,6 +53,6 @@ export default class LidResolve extends BaseCommand { : {usernameKey: flags['username-key']}), }, ) - this.writeValue(result, renderResolvedLid(result.data), flags) + this.writeValue(result, renderResolvedIdentity(result.data), flags) } } diff --git a/src/commands/label/chat/list.ts b/src/commands/label/chat/list.ts index b1dcb1ae..2184d318 100644 --- a/src/commands/label/chat/list.ts +++ b/src/commands/label/chat/list.ts @@ -5,7 +5,10 @@ import {labelItems, listChatLabels} from '../../../services/labels.js' export default class LabelChatList extends BaseCommand { static override args = { - chat: Args.string({description: 'Chat or group JID.', required: true}), + chat: Args.string({ + description: 'Polymorfa conversation ID.', + required: true, + }), } static override description = 'List the labels assigned to a chat.' diff --git a/src/commands/label/chat/replace.ts b/src/commands/label/chat/replace.ts index 548d1bb8..08b27722 100644 --- a/src/commands/label/chat/replace.ts +++ b/src/commands/label/chat/replace.ts @@ -5,7 +5,10 @@ import {replaceChatLabels} from '../../../services/labels.js' export default class LabelChatReplace extends BaseCommand { static override args = { - chat: Args.string({description: 'Chat or group JID.', required: true}), + chat: Args.string({ + description: 'Polymorfa conversation ID.', + required: true, + }), } static override description = "Replace a chat's complete label set." diff --git a/src/commands/message/mark-seen.ts b/src/commands/message/mark-seen.ts index dd9345da..2d1e1d81 100644 --- a/src/commands/message/mark-seen.ts +++ b/src/commands/message/mark-seen.ts @@ -1,4 +1,5 @@ import {Args, Flags} from '@oclif/core' +import {conversationReference} from '../../services/conversation.js' import {BaseCommand} from '../../base-command.js' import {markMessageSeen} from '../../services/messages.js' @@ -11,7 +12,10 @@ export default class MessageMarkSeen extends BaseCommand { static override description = 'Mark a message as seen.' static override flags = { - chat: Flags.string({description: 'Chat or group JID.', required: true}), + chat: Flags.string({ + description: 'Conversation ID, E.164 phone number, or BSUID.', + required: true, + }), 'idempotency-key': Flags.string({ description: 'Safely identify retries of this seen receipt.', }), @@ -26,7 +30,7 @@ export default class MessageMarkSeen extends BaseCommand { const result = await markMessageSeen( await this.messagingClient(flags), flags.session, - {chatId: flags.chat, messageId: args.message}, + {conversation: conversationReference(flags.chat), id: args.message}, flags['idempotency-key'] === undefined ? {} : {idempotencyKey: flags['idempotency-key']}, diff --git a/src/commands/message/react.ts b/src/commands/message/react.ts index f3db1d16..933fc0e5 100644 --- a/src/commands/message/react.ts +++ b/src/commands/message/react.ts @@ -1,4 +1,5 @@ import {Args, Flags} from '@oclif/core' +import {conversationReference} from '../../services/conversation.js' import {BaseCommand} from '../../base-command.js' import {reactToMessage} from '../../services/messages.js' @@ -11,7 +12,10 @@ export default class MessageReact extends BaseCommand { static override description = 'React to a message.' static override flags = { - chat: Flags.string({description: 'Chat or group JID.', required: true}), + chat: Flags.string({ + description: 'Conversation ID, E.164 phone number, or BSUID.', + required: true, + }), emoji: Flags.string({description: 'Reaction emoji.', required: true}), 'idempotency-key': Flags.string({ description: 'Safely identify retries of this reaction.', @@ -27,7 +31,11 @@ export default class MessageReact extends BaseCommand { const result = await reactToMessage( await this.messagingClient(flags), flags.session, - {chatId: flags.chat, messageId: args.message, reaction: flags.emoji}, + { + conversation: conversationReference(flags.chat), + id: args.message, + reaction: flags.emoji, + }, flags['idempotency-key'] === undefined ? {} : {idempotencyKey: flags['idempotency-key']}, diff --git a/src/commands/message/send/text.ts b/src/commands/message/send/text.ts index 30033586..5e04b946 100644 --- a/src/commands/message/send/text.ts +++ b/src/commands/message/send/text.ts @@ -3,6 +3,7 @@ import type {SendTextMessageRequest} from '@polymorfa/sdk' import {BaseCommand} from '../../../base-command.js' import {sendTextMessage} from '../../../services/messages.js' +import {conversationReference} from '../../../services/conversation.js' export default class MessageSendText extends BaseCommand { static override description = 'Send a text message.' @@ -19,23 +20,19 @@ export default class MessageSendText extends BaseCommand { description: 'Safely identify retries of this send operation.', }), mention: Flags.string({ - description: 'Mention a recipient JID. Repeat for multiple recipients.', + description: + 'Mention a Polymorfa user ID. Repeat for multiple recipients.', multiple: true, }), - 'reply-participant': Flags.string({ - dependsOn: ['reply-to'], - description: 'Participant JID for the quoted message.', - }), 'reply-to': Flags.string({ - dependsOn: ['reply-participant'], - description: 'Message ID to quote in the reply.', + description: 'Polymorfa message ID to quote in the reply.', }), session: Flags.string({ description: 'Messaging session ID.', required: true, }), to: Flags.string({ - description: 'Recipient chat or group JID.', + description: 'Conversation ID, E.164 phone number, or BSUID.', required: true, }), } @@ -43,17 +40,15 @@ export default class MessageSendText extends BaseCommand { async run(): Promise { const {flags} = await this.parse(MessageSendText) const request: SendTextMessageRequest = { - chatId: flags.to, - text: flags.body, - type: 'text', + conversation: conversationReference(flags.to), + content: {text: flags.body}, ...(flags.forwarded ? {isForwarded: true} : {}), ...(flags.mention === undefined ? {} : {mentions: flags.mention}), ...(flags['reply-to'] === undefined ? {} : { quotedMessage: { - messageId: flags['reply-to'], - participant: flags['reply-participant']!, + id: flags['reply-to'], }, }), } diff --git a/src/commands/message/star.ts b/src/commands/message/star.ts index ebed4aaa..2a78725b 100644 --- a/src/commands/message/star.ts +++ b/src/commands/message/star.ts @@ -1,4 +1,5 @@ import {Args, Flags} from '@oclif/core' +import {conversationReference} from '../../services/conversation.js' import {BaseCommand} from '../../base-command.js' import {setMessageStar} from '../../services/messages.js' @@ -11,7 +12,10 @@ export default class MessageStar extends BaseCommand { static override description = 'Star or unstar a message.' static override flags = { - chat: Flags.string({description: 'Chat or group JID.', required: true}), + chat: Flags.string({ + description: 'Conversation ID, E.164 phone number, or BSUID.', + required: true, + }), 'idempotency-key': Flags.string({ description: 'Safely identify retries of this star update.', }), @@ -27,7 +31,11 @@ export default class MessageStar extends BaseCommand { const result = await setMessageStar( await this.messagingClient(flags), flags.session, - {chatId: flags.chat, messageId: args.message, star: !flags.remove}, + { + conversation: conversationReference(flags.chat), + id: args.message, + star: !flags.remove, + }, flags['idempotency-key'] === undefined ? {} : {idempotencyKey: flags['idempotency-key']}, diff --git a/src/commands/message/typing.ts b/src/commands/message/typing.ts index 8e9d7ba3..f23b798c 100644 --- a/src/commands/message/typing.ts +++ b/src/commands/message/typing.ts @@ -1,4 +1,5 @@ import {Flags} from '@oclif/core' +import {conversationReference} from '../../services/conversation.js' import {BaseCommand} from '../../base-command.js' import {setMessageTyping} from '../../services/messages.js' @@ -9,7 +10,10 @@ export default class MessageTyping extends BaseCommand { static override description = 'Set the typing state for a chat.' static override flags = { - chat: Flags.string({description: 'Chat or group JID.', required: true}), + chat: Flags.string({ + description: 'Conversation ID, E.164 phone number, or BSUID.', + required: true, + }), 'idempotency-key': Flags.string({ description: 'Safely identify retries of this typing update.', }), @@ -29,7 +33,7 @@ export default class MessageTyping extends BaseCommand { const result = await setMessageTyping( await this.messagingClient(flags), flags.session, - {chatId: flags.chat, state: flags.state}, + {conversation: conversationReference(flags.chat), state: flags.state}, flags['idempotency-key'] === undefined ? {} : {idempotencyKey: flags['idempotency-key']}, diff --git a/src/commands/operation/view.ts b/src/commands/operation/view.ts deleted file mode 100644 index 8768c951..00000000 --- a/src/commands/operation/view.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {Args, Flags} from '@oclif/core' - -import {BaseCommand} from '../../base-command.js' -import { - renderMessagingOperation, - viewMessagingOperation, -} from '../../services/campaigns.js' -import { - renderPlatformOperation, - viewPlatformOperation, -} from '../../services/platform-management.js' - -export default class OperationView extends BaseCommand { - static override args = { - operation: Args.string({ - description: 'Operation ID.', - required: true, - }), - } - - static override description = 'View one durable operation.' - - static override flags = { - surface: Flags.string({ - default: 'platform', - description: 'API surface that owns the operation.', - options: ['platform', 'messaging'], - }), - } - - async run(): Promise { - const {args, flags} = await this.parse(OperationView) - if (flags.surface === 'messaging') { - const result = await viewMessagingOperation( - await this.messagingClient(flags, {serverApiKeyOnly: 'Operation'}), - args.operation, - ) - this.writeValue(result, renderMessagingOperation(result.data), flags) - return - } - - const result = await viewPlatformOperation( - await this.platformClient(flags), - args.operation, - ) - this.writeValue(result, renderPlatformOperation(result.data), flags) - } -} diff --git a/src/commands/presence/chat/subscribe.ts b/src/commands/presence/chat/subscribe.ts index 47ba4ca5..277df910 100644 --- a/src/commands/presence/chat/subscribe.ts +++ b/src/commands/presence/chat/subscribe.ts @@ -5,7 +5,7 @@ import {subscribeToChatPresence} from '../../../services/presence.js' export default class PresenceChatSubscribe extends BaseCommand { static override args = { - chat: Args.string({description: 'User or LID JID.', required: true}), + chat: Args.string({description: 'Polymorfa user ID.', required: true}), } static override description = diff --git a/src/commands/presence/chat/view.ts b/src/commands/presence/chat/view.ts index 1ca69f9b..958ba21d 100644 --- a/src/commands/presence/chat/view.ts +++ b/src/commands/presence/chat/view.ts @@ -9,7 +9,7 @@ import { export default class PresenceChatView extends BaseCommand { static override args = { chat: Args.string({ - description: 'Chat, user, or group JID.', + description: 'Polymorfa conversation ID.', required: true, }), } diff --git a/src/commands/user/security-code.ts b/src/commands/user/security-code.ts index ec4ca072..0d787a87 100644 --- a/src/commands/user/security-code.ts +++ b/src/commands/user/security-code.ts @@ -9,7 +9,7 @@ import { export default class UserSecurityCode extends BaseCommand { static override args = { user: Args.string({ - description: 'Stable user ID in digits@lid form.', + description: 'Decimal Polymorfa user ID.', required: true, }), } diff --git a/src/sdk/client.ts b/src/sdk/client.ts index 87b1e5d3..eb012617 100644 --- a/src/sdk/client.ts +++ b/src/sdk/client.ts @@ -1,6 +1,6 @@ import { MessagingClient, - PlatformClient, + Client as PlatformClient, PolymorfaConfigurationError, type RawClient, type SharedClientOptions, @@ -41,7 +41,10 @@ export function createPlatformClient( } return new PlatformClient({ - apiKey: environment.credential.value, + credential: { + type: 'organizationApiKey', + value: environment.credential.value, + }, ...sharedOptions(environment), }) } diff --git a/src/services/auth.ts b/src/services/auth.ts index 8961b1ac..0b40c8a9 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -1,7 +1,7 @@ import { PolymorfaConfigurationError, PolymorfaNotFoundError, - validatePlatformApiKey, + validateOrganizationApiKey, } from '@polymorfa/sdk' import { @@ -54,7 +54,7 @@ export class AuthService { readonly profile: string }): Promise { assertProfileName(input.profile) - const apiKey = validatePlatformApiKey(input.apiKey.trim()) + const apiKey = validateOrganizationApiKey(input.apiKey.trim()) const config = await this.profiles.load() const previous = config.profiles[input.profile] const previousAccount = previous?.credential.account @@ -133,7 +133,7 @@ export class AuthService { let credentialFormatValid = false if (credential !== undefined) { try { - validatePlatformApiKey(credential) + validateOrganizationApiKey(credential) credentialFormatValid = true } catch { credentialFormatValid = false diff --git a/src/services/business.ts b/src/services/business.ts index 12d499d2..44a60944 100644 --- a/src/services/business.ts +++ b/src/services/business.ts @@ -377,7 +377,7 @@ export async function viewBusinessEligibility( export function renderBusinessProfile(profile: BusinessProfile): string { return [ - row('JID', profile.jid), + row('ID', profile.id), row('Address', profile.address), row('Email', profile.email), row('Description', profile.description), diff --git a/src/services/campaigns.ts b/src/services/campaigns.ts index 8e642b8d..d97876b2 100644 --- a/src/services/campaigns.ts +++ b/src/services/campaigns.ts @@ -5,7 +5,6 @@ import { type CampaignRequeueResult, type CreateCampaignRequest, type MessagingClient, - type Operation, PolymorfaValidationError, type RequestOptions, type ResponseMetadata, @@ -131,14 +130,6 @@ export async function requeueCampaign( return result(response.data.data, response.metadata) } -export async function viewMessagingOperation( - client: MessagingClient, - operationId: string, -): Promise> { - const response = await client.operations.retrieve(operationId) - return result(response.data.data, response.metadata) -} - export function parseCampaignSchedule(value?: string): number | undefined { if (value === undefined) return undefined @@ -206,7 +197,7 @@ export function renderCampaignOperation( return [ `Accepted campaign ${action} as operation ${campaign.operationId}.`, `Campaign ${campaign.id} is ${campaign.status}.`, - `Poll with polymorfa operation view ${campaign.operationId} --surface messaging.`, + 'Check the campaign status with polymorfa campaign view.', ].join('\n') } @@ -214,26 +205,6 @@ export function renderCampaignRequeue(result: CampaignRequeueResult): string { return `Requeued ${result.requeued} ${result.requeued === 1 ? 'recipient' : 'recipients'}.` } -export function renderMessagingOperation(operation: Operation): string { - return [ - `ID\t${operation.id}`, - `Kind\t${operation.kind}`, - `Status\t${operation.status}`, - `Resource\t${operation.resourceType}\t${operation.resourceId}`, - ...(operation.projectId === null - ? [] - : [`Project\t${operation.projectId}`]), - ...(operation.progressCode === null - ? [] - : [`Progress\t${operation.progressCode}`]), - ...(operation.failureCode === null - ? [] - : [`Failure\t${operation.failureCode}`]), - `Created\t${operation.createdAt}`, - `Updated\t${operation.updatedAt}`, - ].join('\n') -} - function campaignInput(input: unknown): CreateCampaignRequest { if (!isRecord(input)) { throw new PolymorfaValidationError( diff --git a/src/services/channels.ts b/src/services/channels.ts index 10a9d6b3..033aee0b 100644 --- a/src/services/channels.ts +++ b/src/services/channels.ts @@ -102,13 +102,13 @@ export async function markChannelMessageViewed( client: MessagingClient, session: string, channel: string, - serverId: string, + messageId: string, options: RequestOptions = {}, ): Promise> { const response = await client.channels.markMessageViewed( session, channel, - parseServerId(serverId), + parseMessageId(messageId), options, ) return result(response.data, response.metadata) @@ -118,7 +118,7 @@ export async function reactToChannelMessage( client: MessagingClient, session: string, channel: string, - serverId: string, + messageId: string, reaction: string, options: RequestOptions = {}, ): Promise> { @@ -132,7 +132,7 @@ export async function reactToChannelMessage( const response = await client.channels.reactToMessage( session, channel, - parseServerId(serverId), + parseMessageId(messageId), {reaction}, options, ) @@ -193,7 +193,7 @@ export function renderChannels(channels: readonly Channel[]): string { if (channels.length === 0) return 'No channels found.' return channels .map((channel) => - [channel.lid ?? 'unknown', channel.name ?? 'Unnamed channel'] + [channel.id ?? 'unknown', channel.name ?? 'Unnamed channel'] .concat( channel.followers === undefined ? [] @@ -207,7 +207,7 @@ export function renderChannels(channels: readonly Channel[]): string { export function renderChannel(channel: Channel): string { const rows = [ row('Name', channel.name), - row('ID', channel.lid), + row('ID', channel.id), row('Description', channel.description), row('Picture', channel.profileUrl), numberRow('Followers', channel.followers), @@ -226,7 +226,7 @@ export function renderChannelMessages( return messages .map((message) => [ - String(message.serverId), + message.id, message.timestamp, message.type, `${message.views} views`, @@ -238,23 +238,15 @@ export function renderChannelMessages( .join('\n') } -function parseServerId(serverId: string): number { - if (!/^[1-9]\d*$/.test(serverId)) { +function parseMessageId(messageId: string): string { + if (!/^[1-9]\d{0,18}$/.test(messageId)) { throw new PolymorfaConfigurationError( - 'Message server ID must be a positive integer.', - 'serverId', + 'Message ID must be an opaque decimal string.', + 'messageId', ) } - const parsed = Number(serverId) - if (!Number.isSafeInteger(parsed)) { - throw new PolymorfaConfigurationError( - 'Message server ID must be a positive safe integer.', - 'serverId', - ) - } - - return parsed + return messageId } async function channelAction( diff --git a/src/services/contacts.ts b/src/services/contacts.ts index 6b738178..e7c4a2e6 100644 --- a/src/services/contacts.ts +++ b/src/services/contacts.ts @@ -115,7 +115,7 @@ export function renderContacts(contacts: readonly Contact[]): string { if (contacts.length === 0) return 'No contacts found.' return contacts .map((contact) => - [contact.lid, contact.name, contact.phoneNumber, contact.businessName] + [contact.id, contact.name, contact.phoneNumber, contact.businessName] .filter((value): value is string => value !== undefined) .join('\t'), ) @@ -127,7 +127,7 @@ export function renderContact(contact: Contact): string { row('Name', contact.name), row('Push name', contact.pushName), row('Business name', contact.businessName), - row('LID', contact.lid), + row('BSUID', contact.bsuid), row('ID', contact.id), row('Phone', contact.phoneNumber), row('Username', contact.username), @@ -143,9 +143,9 @@ export function renderContactChecks( return checks .map((check) => [ - check.phoneNumber ?? check.id ?? check.lid ?? 'unknown', + check.phoneNumber ?? check.id ?? 'unknown', check.exists ? 'registered' : 'not registered', - check.lid, + check.id, ] .filter((value): value is string => value !== undefined) .join('\t'), @@ -155,15 +155,14 @@ export function renderContactChecks( export function renderContactInfo(info: ContactUserInfo): string { return [ - row('JID', info.jid), - row('LID', info.lid), + row('BSUID', info.bsuid), row('ID', info.id), row('Phone', info.phoneNumber), row('Username', info.username), row('Status', info.status), row('Picture ID', info.pictureId), row('Verified name', info.verifiedName), - ...info.devices.map((device) => `Device\t${device}`), + ...info.devices.map((device) => `Device\t${device.id}\t${device.device}`), ] .filter((value): value is string => value !== undefined) .join('\n') @@ -171,7 +170,8 @@ export function renderContactInfo(info: ContactUserInfo): string { export function renderBusinessProfile(profile: BusinessProfile): string { return [ - row('JID', profile.jid), + row('ID', profile.id), + row('BSUID', profile.bsuid), row('Description', profile.description), row('Address', profile.address), row('Email', profile.email), @@ -194,7 +194,7 @@ export function renderBusinessProfile(profile: BusinessProfile): string { export function renderBlocklist(blocklist: ContactBlocklist): string { return [ `Hash\t${blocklist.hash}`, - ...blocklist.jids.map((jid) => `Blocked\t${jid}`), + ...blocklist.contacts.map((contact) => `Blocked\t${contact.id}`), ].join('\n') } diff --git a/src/services/conversation.ts b/src/services/conversation.ts new file mode 100644 index 00000000..5aaedce2 --- /dev/null +++ b/src/services/conversation.ts @@ -0,0 +1,14 @@ +import {PolymorfaConfigurationError} from '@polymorfa/sdk' + +/** CLI input never accepts raw provider addresses. */ +export function conversationReference( + value: string, +): {id: string} | {phoneNumber: string} | {bsuid: string} { + if (/^\+[1-9]\d{1,14}$/.test(value)) return {phoneNumber: value} + if (/^[1-9]\d{0,18}$/.test(value)) return {id: value} + if (/^[A-Z]{2}\.[A-Za-z0-9]{1,128}$/.test(value)) return {bsuid: value} + throw new PolymorfaConfigurationError( + 'Use a Polymorfa conversation ID, E.164 phone number, or BSUID.', + 'conversation', + ) +} diff --git a/src/services/customers.ts b/src/services/customers.ts index 204c1131..55504f2c 100644 --- a/src/services/customers.ts +++ b/src/services/customers.ts @@ -12,7 +12,7 @@ import { type CustomersStatus, type ListCustomerEventsParams, type ListCustomersParams, - type PlatformClient, + type Client as PlatformClient, PolymorfaConfigurationError, PolymorfaValidationError, type RequestOptions, diff --git a/src/services/messages.ts b/src/services/messages.ts index b5bb1913..54c79260 100644 --- a/src/services/messages.ts +++ b/src/services/messages.ts @@ -1,6 +1,7 @@ import { PolymorfaConfigurationError, type MessageResponse, + type MessageReceipt, type MessagingClient, type ReactRequest, type RequestOptions, @@ -28,7 +29,7 @@ export async function sendTextMessage( request: SendTextMessageRequest, options: RequestOptions = {}, ): Promise> { - if (request.text?.trim().length === 0 || request.text === undefined) { + if (request.content.text.trim().length === 0) { throw new PolymorfaConfigurationError( 'Text message body cannot be empty.', 'body', @@ -43,7 +44,7 @@ export async function reactToMessage( session: string, request: ReactRequest, options: RequestOptions = {}, -): Promise> { +): Promise> { const response = await client.messages.react(session, request, options) return result(response.data.data, response.metadata) } @@ -73,7 +74,7 @@ export async function setMessageStar( session: string, request: StarRequest, options: RequestOptions = {}, -): Promise> { +): Promise> { const response = await client.messages.star(session, request, options) return result(response.data.data, response.metadata) } diff --git a/src/services/platform-management.ts b/src/services/platform-management.ts index 1d17c115..162419c6 100644 --- a/src/services/platform-management.ts +++ b/src/services/platform-management.ts @@ -3,9 +3,8 @@ import { type ApiKeyDeactivation, type AuditLog, type ListAuditLogsParams, - type ManagementOperation, type OrganizationMember, - type PlatformClient, + type Client as PlatformClient, PolymorfaValidationError, type ProjectToken, type RequestOptions, @@ -16,10 +15,13 @@ import { type SessionBatchRemoveResult, type SessionBatchRequest, type SessionBatchStopResult, - type UpdateWidgetSettingsRequest, - type WidgetSettings, + type UpdateQuickLinkSettingsInput, + type OrganizationQuickLinkSettings, + type ProjectQuickLinkSettings, } from '@polymorfa/sdk' +type WidgetSettings = OrganizationQuickLinkSettings | ProjectQuickLinkSettings + export interface PlatformResult { readonly data: T readonly metadata: { @@ -90,14 +92,6 @@ export async function acknowledgeSecurityIncident( return result(response.data.data, response.metadata) } -export async function viewPlatformOperation( - client: PlatformClient, - operationId: string, -): Promise> { - const response = await client.operations.retrieve(operationId) - return result(response.data.data, response.metadata) -} - export async function listProjectTokens( client: PlatformClient, projectId: string, @@ -110,10 +104,10 @@ export async function viewWidgetSettings( client: PlatformClient, projectId?: string, ): Promise> { - const response = await client.widgetSettings.retrieve( - projectId === undefined ? {} : {projectId}, - ) - return result(response.data.data, response.metadata) + const response = await ( + projectId === undefined ? client : client.project(projectId) + ).quickLinkSettings.retrieve() + return result(response.data, response.metadata) } export async function updateWidgetSettings( @@ -122,11 +116,10 @@ export async function updateWidgetSettings( projectId?: string, options: RequestOptions = {}, ): Promise> { - const response = await client.widgetSettings.update( - widgetUpdate(input, projectId), - options, - ) - return result(response.data.data, response.metadata) + const response = await ( + projectId === undefined ? client : client.project(projectId) + ).quickLinkSettings.update(widgetUpdate(input), options) + return result(response.data, response.metadata) } export async function stopSessions( @@ -224,28 +217,6 @@ export function renderSecurityIncidents( .join('\n') } -export function renderPlatformOperation( - operation: ManagementOperation, -): string { - return [ - `ID\t${operation.id}`, - `Kind\t${operation.kind}`, - `Status\t${operation.status}`, - `Resource\t${operation.resourceType}\t${operation.resourceId}`, - ...(operation.projectId === null - ? [] - : [`Project\t${operation.projectId}`]), - ...(operation.progressCode === null - ? [] - : [`Progress\t${operation.progressCode}`]), - ...(operation.failureCode === null - ? [] - : [`Failure\t${operation.failureCode}`]), - `Created\t${operation.createdAt}`, - `Updated\t${operation.updatedAt}`, - ].join('\n') -} - export function renderProjectTokens(tokens: readonly ProjectToken[]): string { if (tokens.length === 0) return 'No project tokens found.' return tokens @@ -274,16 +245,14 @@ export function renderWidgetSettings( `ID\t${settings.id}`, `Scope\t${settings.projectId ?? 'organization defaults'}`, `Enabled\t${settings.enabled}`, - `Modes\t${settings.modesAllowed.join(', ')}`, `Methods\t${settings.methods?.join(', ') ?? 'default'}`, `Theme\t${settings.theme}`, `Business name\t${settings.businessName ?? ''}`, - `Allowed origins\t${settings.allowedOrigins.join(', ')}`, `Redirect URIs\t${settings.allowedRedirectUris.join(', ')}`, `Hide watermark\t${settings.hideWatermark}`, `Shape\t${settings.shape ?? 'default'}`, `Radius\t${settings.radiusPx ?? 'default'}`, - `QR logo\t${settings.qrLogoMode}`, + `Logo\t${settings.logoMode}`, `History sync\t${settings.historySync}`, ].join('\n') } @@ -317,10 +286,7 @@ function timestamp(value: number): string { return new Date(milliseconds).toISOString() } -function widgetUpdate( - input: unknown, - projectId?: string, -): UpdateWidgetSettingsRequest { +function widgetUpdate(input: unknown): UpdateQuickLinkSettingsInput { if (!isRecord(input)) { throw new PolymorfaValidationError( 'Widget input must contain one JSON object.', @@ -336,7 +302,6 @@ function widgetUpdate( return { ...input, - ...(projectId === undefined ? {} : {projectId}), } } diff --git a/src/services/session-identities.ts b/src/services/session-identities.ts index c9178a3b..d72de311 100644 --- a/src/services/session-identities.ts +++ b/src/services/session-identities.ts @@ -3,8 +3,8 @@ import { type MessagingClient, type RejectCallResponse, type RequestOptions, - type ResolveLidParams, - type ResolveLidResult, + type ResolveIdentityParams, + type ResolveIdentityResult, type ResponseMetadata, type UserSecurityCode, } from '@polymorfa/sdk' @@ -37,7 +37,7 @@ export async function rejectCall( } if (from.trim().length === 0) { throw new PolymorfaConfigurationError( - 'Incoming caller JID cannot be empty.', + 'Incoming caller Polymorfa ID cannot be empty.', 'from', ) } @@ -51,18 +51,17 @@ export async function rejectCall( return result(response.data, response.metadata) } -export async function resolveLid( +export async function resolveIdentity( client: MessagingClient, session: string, input: { readonly id?: string - readonly lid?: string readonly phoneNumber?: string readonly username?: string readonly usernameKey?: string }, -): Promise> { - const response = await client.lids.resolve(session, lidParams(input)) +): Promise> { + const response = await client.identities.resolve(session, lidParams(input)) return result(response.data.data, response.metadata) } @@ -71,14 +70,17 @@ export async function viewUserSecurityCode( session: string, userId: string, ): Promise> { - validateLid(userId, 'User ID') + validateUserId(userId, 'User ID') const response = await client.users.getSecurityCode(session, userId) return result(response.data.data, response.metadata) } -export function renderResolvedLid(identity: ResolveLidResult): string { +export function renderResolvedIdentity( + identity: ResolveIdentityResult, +): string { const rows = [ - row('ID', identity.id ?? identity.lid), + row('ID', identity.id), + row('BSUID', identity.bsuid), row('Phone', identity.phoneNumber), row('Username', identity.username), identity.keyRequired === undefined @@ -110,20 +112,16 @@ export function renderCallRejection(response: RejectCallResponse): string { function lidParams(input: { readonly id?: string - readonly lid?: string readonly phoneNumber?: string readonly username?: string readonly usernameKey?: string -}): ResolveLidParams { - const identities = [ - input.phoneNumber, - input.id, - input.lid, - input.username, - ].filter((value) => value !== undefined) +}): ResolveIdentityParams { + const identities = [input.phoneNumber, input.id, input.username].filter( + (value) => value !== undefined, + ) if (identities.length !== 1) { throw new PolymorfaConfigurationError( - 'Provide exactly one of --phone-number, --id, --lid, or --username.', + 'Provide exactly one of --phone-number, --id, or --username.', 'identity', ) } @@ -139,13 +137,9 @@ function lidParams(input: { return {phoneNumber} } if (input.id !== undefined) { - validateLid(input.id, 'LID') + validateUserId(input.id, 'User ID') return {id: input.id} } - if (input.lid !== undefined) { - validateLid(input.lid, 'LID') - return {lid: input.lid} - } const username = input.username! if (username.length < 3 || username.length > 35) { @@ -168,10 +162,10 @@ function lidParams(input: { } } -function validateLid(value: string, label: string): void { - if (!/^\d+@lid$/.test(value)) { +function validateUserId(value: string, label: string): void { + if (!/^[1-9]\d{0,18}$/.test(value)) { throw new PolymorfaConfigurationError( - `${label} must use the digits@lid form.`, + `${label} must be a decimal string.`, 'id', ) } diff --git a/test/commands/api.test.ts b/test/commands/api.test.ts index d72caca2..2b6c0601 100644 --- a/test/commands/api.test.ts +++ b/test/commands/api.test.ts @@ -32,7 +32,8 @@ describe('api', () => { const result = await withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, @@ -52,7 +53,7 @@ describe('api', () => { url: '/v1/projects', }) expect(server.requests[0]?.headers.authorization).toBe( - 'Bearer pmfa_test-key', + 'Bearer pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', ) }) @@ -64,7 +65,8 @@ describe('api', () => { const result = await withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, @@ -93,7 +95,8 @@ describe('api', () => { const result = await withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, @@ -115,7 +118,8 @@ describe('api', () => { const result = await withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, @@ -135,14 +139,18 @@ describe('api', () => { test('redacts credentials from API errors and preserves the request ID', async () => { server = await startHttpServer({ - body: {error: 'Invalid API key pmfa_test-key.'}, + body: { + error: + 'Invalid API key pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.', + }, headers: {'request-id': 'req_auth'}, status: 401, }) const result = await withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, @@ -156,7 +164,9 @@ describe('api', () => { message: 'Invalid API key [REDACTED].', requestId: 'req_auth', }) - expect(result.stderr).not.toContain('pmfa_test-key') + expect(result.stderr).not.toContain( + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + ) expect(result.stdout).toBe('') }) @@ -165,7 +175,8 @@ describe('api', () => { const result = await withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, @@ -188,7 +199,8 @@ describe('api', () => { const result = await withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, @@ -215,7 +227,8 @@ describe('api', () => { const result = await withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, @@ -248,7 +261,8 @@ describe('api', () => { const result = await withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, @@ -265,7 +279,8 @@ describe('api', () => { const result = await withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, @@ -284,7 +299,8 @@ describe('api', () => { const result = await withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, diff --git a/test/commands/auth.test.ts b/test/commands/auth.test.ts index acdf8e68..5fbddeda 100644 --- a/test/commands/auth.test.ts +++ b/test/commands/auth.test.ts @@ -59,8 +59,10 @@ describe('auth commands', () => { test('logs in from stdin, lists profiles, and switches active profile', async () => { await withConfig(async () => { - const login = await withStdin('pmfa_test-key\n', async () => - runCommand('auth login --with-api-key --profile work --json'), + const login = await withStdin( + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n', + async () => + runCommand('auth login --with-api-key --profile work --json'), ) expect(login.error).toBeUndefined() expect(JSON.parse(login.stdout)).toEqual({ @@ -81,8 +83,10 @@ describe('auth commands', () => { test('reports keyring availability and logs out only the selected profile', async () => { await withConfig(async () => { - await withStdin('pmfa_test-key\n', async () => - runCommand('auth login --with-api-key --profile work --json'), + await withStdin( + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n', + async () => + runCommand('auth login --with-api-key --profile work --json'), ) const status = await runCommand('auth status --profile work --json') @@ -105,7 +109,7 @@ describe('auth commands', () => { test('reports environment credential validation separately from remote exercise', async () => { await withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_environment-key', + POLYMORFA_API_KEY: `pmfa_${'A'.repeat(72)}`, POLYMORFA_CLIENT_TOKEN: undefined, }, async () => { diff --git a/test/commands/business.test.ts b/test/commands/business.test.ts index 51361a0b..9bf7cd06 100644 --- a/test/commands/business.test.ts +++ b/test/commands/business.test.ts @@ -57,7 +57,9 @@ describe('business commands', () => { status: 200, }, }) - expect(server.requests[0]?.url).toBe('/api/session%2Fmain/business/profile') + expect(server.requests[0]?.url).toBe( + '/messaging/session%2Fmain/business/profile', + ) }) test('updates structured profile fields from one bounded JSON input', async () => { @@ -93,7 +95,7 @@ describe('business commands', () => { }) expect(server.requests[0]).toMatchObject({ method: 'PATCH', - url: '/api/session_1/business/profile', + url: '/messaging/session_1/business/profile', }) expect(server.requests[0]?.headers['idempotency-key']).toBe('profile_1') }) @@ -123,12 +125,12 @@ describe('business commands', () => { base64: '/9j/2Q==', }) expect(server.requests[0]?.url).toBe( - '/api/session%2Fmain/business/profile/cover-photo', + '/messaging/session%2Fmain/business/profile/cover-photo', ) expect(deleteResult.error).toBeUndefined() expect(server.requests[1]).toMatchObject({ method: 'DELETE', - url: '/api/session%2Fmain/business/profile/cover-photo/cover%2F1', + url: '/messaging/session%2Fmain/business/profile/cover-photo/cover%2F1', }) }) @@ -139,13 +141,13 @@ describe('business commands', () => { const result = await apiKeyEnvironment(() => runCommand( - 'business catalog view --session session_1 --jid shop@lid --after cursor_1 --limit 25 --width 320 --height 240 --json', + 'business catalog view --session session_1 --id 750000000001 --after cursor_1 --limit 25 --width 320 --height 240 --json', ), ) expect(result.error).toBeUndefined() expect(server.requests[0]?.url).toBe( - '/api/session_1/business/catalog?jid=shop%40lid&after=cursor_1&limit=25&width=320&height=240', + '/messaging/session_1/business/catalog?id=750000000001&after=cursor_1&limit=25&width=320&height=240', ) }) @@ -169,13 +171,13 @@ describe('business commands', () => { expect(createResult.error).toBeUndefined() expect(server.requests[0]).toMatchObject({ method: 'POST', - url: '/api/session_1/business/catalog', + url: '/messaging/session_1/business/catalog', }) expect(cartResult.error).toBeUndefined() expect(server.requests[1]).toMatchObject({ body: JSON.stringify({enabled: false}), method: 'PATCH', - url: '/api/session_1/business/catalog/cart', + url: '/messaging/session_1/business/catalog/cart', }) }) @@ -184,13 +186,13 @@ describe('business commands', () => { const result = await apiKeyEnvironment(() => runCommand( - 'business product view product/1 --session session/main --jid shop@lid --json', + 'business product view product/1 --session session/main --id 750000000001 --json', ), ) expect(result.error).toBeUndefined() expect(server.requests[0]?.url).toBe( - '/api/session%2Fmain/business/products/product%2F1?jid=shop%40lid', + '/messaging/session%2Fmain/business/products/product%2F1?id=750000000001', ) }) @@ -210,7 +212,7 @@ describe('business commands', () => { ) expect(server.requests[0]).toMatchObject({ method: 'POST', - url: '/api/session_1/business/products', + url: '/messaging/session_1/business/products', }) }) @@ -283,12 +285,12 @@ describe('business commands', () => { expect(replaceResult.error).toBeUndefined() expect(server.requests[0]).toMatchObject({ method: 'PUT', - url: '/api/session_1/business/products/product%2F1', + url: '/messaging/session_1/business/products/product%2F1', }) expect(deleteResult.error).toBeUndefined() expect(server.requests[1]).toMatchObject({ method: 'DELETE', - url: '/api/session_1/business/products/product%2F1', + url: '/messaging/session_1/business/products/product%2F1', }) }) @@ -313,13 +315,13 @@ describe('business commands', () => { expect(server.requests[0]).toMatchObject({ body: JSON.stringify({hidden: true}), method: 'PATCH', - url: '/api/session_1/business/products/product_1/visibility', + url: '/messaging/session_1/business/products/product_1/visibility', }) expect(appealResult.error).toBeUndefined() expect(server.requests[1]).toMatchObject({ body: JSON.stringify({reason: 'Please review'}), method: 'POST', - url: '/api/session_1/business/products/product_1/appeal', + url: '/messaging/session_1/business/products/product_1/appeal', }) }) @@ -330,13 +332,13 @@ describe('business commands', () => { const result = await apiKeyEnvironment(() => runCommand( - 'business collection list --session session_1 --jid shop@lid --after next_1 --collection-limit 10 --item-limit 40 --width 128 --height 96 --json', + 'business collection list --session session_1 --id 750000000001 --after next_1 --collection-limit 10 --item-limit 40 --width 128 --height 96 --json', ), ) expect(result.error).toBeUndefined() expect(server.requests[0]?.url).toBe( - '/api/session_1/business/collections?jid=shop%40lid&after=next_1&collectionLimit=10&itemLimit=40&width=128&height=96', + '/messaging/session_1/business/collections?id=750000000001&after=next_1&collectionLimit=10&itemLimit=40&width=128&height=96', ) }) @@ -345,13 +347,13 @@ describe('business commands', () => { const result = await apiKeyEnvironment(() => runCommand( - 'business collection view collection/1 --session session_1 --jid shop@lid --after product_cursor --limit 20 --width 256 --height 256 --json', + 'business collection view collection/1 --session session_1 --id 750000000001 --after product_cursor --limit 20 --width 256 --height 256 --json', ), ) expect(result.error).toBeUndefined() expect(server.requests[0]?.url).toBe( - '/api/session_1/business/collections/collection%2F1?jid=shop%40lid&after=product_cursor&limit=20&width=256&height=256', + '/messaging/session_1/business/collections/collection%2F1?id=750000000001&after=product_cursor&limit=20&width=256&height=256', ) }) @@ -389,7 +391,7 @@ describe('business commands', () => { productIds: ['product_1', 'product_2'], }), method: 'POST', - url: '/api/session_1/business/collections', + url: '/messaging/session_1/business/collections', }) expect(updateResult.error).toBeUndefined() expect(JSON.parse(server.requests[1]?.body ?? '')).toEqual({ @@ -399,7 +401,7 @@ describe('business commands', () => { }) expect(server.requests[1]).toMatchObject({ method: 'PATCH', - url: '/api/session_1/business/collections/collection_1', + url: '/messaging/session_1/business/collections/collection_1', }) }) @@ -446,7 +448,7 @@ describe('business commands', () => { expect(deleteResult.error).toBeUndefined() expect(server.requests[0]).toMatchObject({ method: 'DELETE', - url: '/api/session_1/business/collections/collection%2F1', + url: '/messaging/session_1/business/collections/collection%2F1', }) expect(reorderResult.error).toBeUndefined() expect(server.requests[1]).toMatchObject({ @@ -454,11 +456,11 @@ describe('business commands', () => { moves: [{collectionId: 'collection_1', fromIndex: 0, toIndex: 1}], }), method: 'POST', - url: '/api/session_1/business/collections/reorder', + url: '/messaging/session_1/business/collections/reorder', }) expect(appealResult.error).toBeUndefined() expect(server.requests[2]?.url).toBe( - '/api/session_1/business/collections/collection%2F1/appeal', + '/messaging/session_1/business/collections/collection%2F1/appeal', ) }) @@ -475,7 +477,7 @@ describe('business commands', () => { expect(server.requests[0]).toMatchObject({ body: JSON.stringify({token: 'event_token'}), method: 'POST', - url: '/api/session%2Fmain/business/orders/order%2F1/lookup', + url: '/messaging/session%2Fmain/business/orders/order%2F1/lookup', }) }) @@ -496,12 +498,14 @@ describe('business commands', () => { ) expect(viewResult.error).toBeUndefined() - expect(server.requests[0]?.url).toBe('/api/session_1/business/compliance') + expect(server.requests[0]?.url).toBe( + '/messaging/session_1/business/compliance', + ) expect(replaceResult.error).toBeUndefined() expect(server.requests[1]).toMatchObject({ body: JSON.stringify(compliance()), method: 'PUT', - url: '/api/session_1/business/compliance', + url: '/messaging/session_1/business/compliance', }) }) @@ -524,7 +528,7 @@ describe('business commands', () => { expect(result.error).toBeUndefined() expect(server.requests[0]?.url).toBe( - `/api/session_1/business/${endpoint}`, + `/messaging/session_1/business/${endpoint}`, ) }, ) @@ -540,7 +544,8 @@ describe('business commands', () => { if (server === undefined) throw new Error('Test server was not started.') return withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, @@ -570,7 +575,7 @@ function businessProfile() { email: 'shop@example.com', hours: [], hoursTimeZone: 'Europe/Zurich', - jid: 'shop@lid', + id: '750000000001', options: {}, websites: ['https://example.com'], } diff --git a/test/commands/calls-and-identities.test.ts b/test/commands/calls-and-identities.test.ts index 72ab8a6f..337e3c28 100644 --- a/test/commands/calls-and-identities.test.ts +++ b/test/commands/calls-and-identities.test.ts @@ -35,7 +35,7 @@ describe('call and identity commands', () => { expect(server.requests[0]).toMatchObject({ body: JSON.stringify({from: 'caller@lid'}), method: 'POST', - url: '/api/session%2Fmain/calls/call%2F1/reject', + url: '/messaging/session%2Fmain/calls/call%2F1/reject', }) expect(server.requests[0]?.headers['idempotency-key']).toBe('reject_1') }) @@ -68,37 +68,37 @@ describe('call and identity commands', () => { test('resolves a trimmed phone number through one exact identity query', async () => { server = await startHttpServer({ body: { - data: {id: '15551234567@lid', phoneNumber: '+15551234567'}, + data: {id: '750000000001', phoneNumber: '+15551234567'}, success: true, }, }) const result = await apiKeyEnvironment(() => runCommand( - 'lid resolve --session session_1 --phone-number " +15551234567 " --json', + 'identity resolve --session session_1 --phone-number " +15551234567 " --json', ), ) expect(result.error).toBeUndefined() expect(server.requests[0]?.url).toBe( - '/api/session_1/lids/resolve?phoneNumber=%2B15551234567', + '/messaging/session_1/identities/resolve?phoneNumber=%2B15551234567', ) }) test('resolves a username with its exact optional four-digit key', async () => { server = await startHttpServer({ - body: {data: {id: '15551234567@lid', username: 'alice'}, success: true}, + body: {data: {id: '750000000001', username: 'alice'}, success: true}, }) const result = await apiKeyEnvironment(() => runCommand( - 'lid resolve --session session_1 --username alice --username-key 1234 --json', + 'identity resolve --session session_1 --username alice --username-key 1234 --json', ), ) expect(result.error).toBeUndefined() expect(server.requests[0]?.url).toBe( - '/api/session_1/lids/resolve?username=alice&usernameKey=1234', + '/messaging/session_1/identities/resolve?username=alice&usernameKey=1234', ) }) @@ -106,12 +106,12 @@ describe('call and identity commands', () => { server = await startHttpServer({body: {data: {}, success: true}}) const result = await apiKeyEnvironment(() => - runCommand('lid resolve --session session_1 --json'), + runCommand('identity resolve --session session_1 --json'), ) expect(exitCode(result.error)).toBe(2) expect(result.stderr).toContain( - 'Provide exactly one of --phone-number, --id, --lid, or --username', + 'Provide exactly one of --phone-number, --id, or --username', ) expect(server.requests).toHaveLength(0) }) @@ -121,7 +121,7 @@ describe('call and identity commands', () => { server = await startHttpServer({ body: { data: { - id: '15551234567@lid', + id: '750000000001', numericCode, phoneNumber: '+15551234567', qrCode: 'cHJpdmF0ZS1kaXNwbGF5LXFy', @@ -133,15 +133,15 @@ describe('call and identity commands', () => { }) const result = await apiKeyEnvironment(() => - runCommand('user security-code 15551234567@lid --session session/main'), + runCommand('user security-code 750000000001 --session session/main'), ) expect(result.error).toBeUndefined() - expect(result.stdout).toContain('ID\t15551234567@lid') + expect(result.stdout).toContain('ID\t750000000001') expect(result.stdout).toContain(`Numeric code\t${numericCode}`) expect(result.stdout).not.toContain('cHJpdmF0ZS1kaXNwbGF5LXFy') expect(server.requests[0]?.url).toBe( - '/api/session%2Fmain/users/15551234567%40lid/security-code', + '/messaging/session%2Fmain/users/750000000001/security-code', ) }) @@ -155,7 +155,7 @@ describe('call and identity commands', () => { ) expect(exitCode(result.error)).toBe(2) - expect(result.stderr).toContain('User ID must use the digits@lid form') + expect(result.stderr).toContain('User ID must be a decimal string') expect(server.requests).toHaveLength(0) }) @@ -163,7 +163,8 @@ describe('call and identity commands', () => { if (server === undefined) throw new Error('Test server was not started.') return withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, diff --git a/test/commands/campaigns.test.ts b/test/commands/campaigns.test.ts index dd50b0ac..fb32d6f9 100644 --- a/test/commands/campaigns.test.ts +++ b/test/commands/campaigns.test.ts @@ -40,7 +40,7 @@ describe('Messaging campaign commands', () => { metadata: {attempts: 1, requestId: 'req_campaign_list', status: 200}, }) expect(server.requests[0]?.url).toBe( - '/api/projects/project%2Fmain/campaigns', + '/messaging/projects/project%2Fmain/campaigns', ) }) @@ -68,7 +68,7 @@ describe('Messaging campaign commands', () => { }) expect(server.requests[0]).toMatchObject({ method: 'POST', - url: '/api/projects/project_1/campaigns', + url: '/messaging/projects/project_1/campaigns', }) expect(server.requests[0]?.headers['idempotency-key']).toBe( 'campaign_create', @@ -108,8 +108,8 @@ describe('Messaging campaign commands', () => { expect(view.error).toBeUndefined() expect(analytics.error).toBeUndefined() expect(server.requests.map((request) => request.url)).toEqual([ - '/api/projects/project%2Fmain/campaigns/campaign%2F1', - '/api/projects/project%2Fmain/campaigns/campaign%2F1/analytics', + '/messaging/projects/project%2Fmain/campaigns/campaign%2F1', + '/messaging/projects/project%2Fmain/campaigns/campaign%2F1/analytics', ]) }) @@ -132,13 +132,13 @@ describe('Messaging campaign commands', () => { 'Accepted campaign launch as operation operation_1.', ) expect(result.stdout).toContain( - 'Poll with polymorfa operation view operation_1 --surface messaging.', + 'Check the campaign status with polymorfa campaign view.', ) expect(result.stdout).not.toContain('Campaign launched successfully') expect(server.requests[0]).toMatchObject({ body: JSON.stringify({scheduledAt: 1_788_264_000_000}), method: 'POST', - url: '/api/projects/project_1/campaigns/campaign_1/launch', + url: '/messaging/projects/project_1/campaigns/campaign_1/launch', }) }) @@ -185,9 +185,9 @@ describe('Messaging campaign commands', () => { expect(resume.error).toBeUndefined() expect(stop.error).toBeUndefined() expect(server.requests.map((request) => request.url)).toEqual([ - '/api/projects/project_1/campaigns/campaign_1/pause', - '/api/projects/project_1/campaigns/campaign_1/resume', - '/api/projects/project_1/campaigns/campaign_1/stop', + '/messaging/projects/project_1/campaigns/campaign_1/pause', + '/messaging/projects/project_1/campaigns/campaign_1/resume', + '/messaging/projects/project_1/campaigns/campaign_1/stop', ]) expect( server.requests.map((request) => request.headers['idempotency-key']), @@ -220,11 +220,11 @@ describe('Messaging campaign commands', () => { expect(server.requests[0]).toMatchObject({ body: JSON.stringify({includeSkippedError: true}), method: 'POST', - url: '/api/projects/project_1/campaigns/campaign_1/requeue', + url: '/messaging/projects/project_1/campaigns/campaign_1/requeue', }) }) - test('polls accepted campaign operations on the Messaging surface', async () => { + test('does not expose console-only operation polling', async () => { server = await startHttpServer({ body: { data: { @@ -248,8 +248,8 @@ describe('Messaging campaign commands', () => { runCommand('operation view operation/1 --surface messaging --json'), ) - expect(result.error).toBeUndefined() - expect(server.requests[0]?.url).toBe('/api/operations/operation%2F1') + expect(result.error).toBeDefined() + expect(server.requests).toEqual([]) }) test('rejects browser client tokens before campaign transport', async () => { @@ -277,7 +277,8 @@ describe('Messaging campaign commands', () => { if (server === undefined) throw new Error('Test server was not started.') return withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, diff --git a/test/commands/channels.test.ts b/test/commands/channels.test.ts index 8f9bc3d4..742587f8 100644 --- a/test/commands/channels.test.ts +++ b/test/commands/channels.test.ts @@ -30,7 +30,7 @@ describe('channel commands', () => { data: [channel()], metadata: {attempts: 1, requestId: 'req_channels', status: 200}, }) - expect(server.requests[0]?.url).toBe('/api/session%2Fmain/channels') + expect(server.requests[0]?.url).toBe('/messaging/session%2Fmain/channels') }) test('renders optional channel metadata without assuming every field exists', async () => { @@ -39,16 +39,16 @@ describe('channel commands', () => { }) const result = await apiKeyEnvironment(() => - runCommand('channel view 120363@newsletter --session session_1'), + runCommand('channel view 760000000001 --session session_1'), ) expect(result.error).toBeUndefined() expect(result.stdout).toContain('Polymorfa updates') - expect(result.stdout).toContain('120363@newsletter') + expect(result.stdout).toContain('760000000001') expect(result.stdout).toContain('Followers\t42') expect(result.stdout).toContain('Muted\tno') expect(server.requests[0]?.url).toBe( - '/api/session_1/channels/120363%40newsletter', + '/messaging/session_1/channels/760000000001', ) }) @@ -81,7 +81,7 @@ describe('channel commands', () => { }) expect(server.requests[0]).toMatchObject({ method: 'POST', - url: '/api/session_1/channels', + url: '/messaging/session_1/channels', }) expect(server.requests[0]?.headers['idempotency-key']).toBe( 'channel_create_1', @@ -107,7 +107,7 @@ describe('channel commands', () => { const result = await apiKeyEnvironment(() => runCommand( - 'channel delete 120363@newsletter --session session_1 --yes --idempotency-key channel_delete_1 --json', + 'channel delete 760000000001 --session session_1 --yes --idempotency-key channel_delete_1 --json', ), ) @@ -118,7 +118,7 @@ describe('channel commands', () => { expect(server.requests[0]).toMatchObject({ body: '', method: 'DELETE', - url: '/api/session_1/channels/120363%40newsletter', + url: '/messaging/session_1/channels/760000000001', }) expect(server.requests[0]?.headers['idempotency-key']).toBe( 'channel_delete_1', @@ -133,7 +133,7 @@ describe('channel commands', () => { const result = await apiKeyEnvironment(() => runCommand( - 'channel message list 120363@newsletter --session session/main --limit 25 --before 951 --json', + 'channel message list 760000000001 --session session/main --limit 25 --before 951 --json', ), ) @@ -147,7 +147,7 @@ describe('channel commands', () => { }, }) expect(server.requests[0]?.url).toBe( - '/api/session%2Fmain/channels/120363%40newsletter/messages?count=25&before=951', + '/messaging/session%2Fmain/channels/760000000001/messages?count=25&before=951', ) }) @@ -158,7 +158,7 @@ describe('channel commands', () => { const result = await apiKeyEnvironment(() => runCommand( - 'channel message updates 120363@newsletter --session session_1 --limit 10 --since 1787212800 --after 951 --json', + 'channel message updates 760000000001 --session session_1 --limit 10 --since 1787212800 --after 951 --json', ), ) @@ -168,7 +168,7 @@ describe('channel commands', () => { metadata: {attempts: 1, status: 200}, }) expect(server.requests[0]?.url).toBe( - '/api/session_1/channels/120363%40newsletter/message-updates?count=10&since=1787212800&after=951', + '/messaging/session_1/channels/760000000001/message-updates?count=10&since=1787212800&after=951', ) }) @@ -177,7 +177,7 @@ describe('channel commands', () => { const result = await apiKeyEnvironment(() => runCommand( - 'channel message list 120363@newsletter --session session_1 --limit 101 --json', + 'channel message list 760000000001 --session session_1 --limit 101 --json', ), ) @@ -188,14 +188,14 @@ describe('channel commands', () => { expect(server.requests).toHaveLength(0) }) - test('marks a positive numeric server ID viewed with idempotency', async () => { + test('marks a public message ID viewed with idempotency', async () => { server = await startHttpServer({ body: {data: {status: 'VIEWED'}, success: true}, }) const result = await apiKeyEnvironment(() => runCommand( - 'channel message mark-viewed 120363@newsletter 951 --session session_1 --idempotency-key viewed_951 --json', + 'channel message mark-viewed 760000000001 951 --session session_1 --idempotency-key viewed_951 --json', ), ) @@ -207,23 +207,23 @@ describe('channel commands', () => { expect(server.requests[0]).toMatchObject({ body: '', method: 'POST', - url: '/api/session_1/channels/120363%40newsletter/messages/951/viewed', + url: '/messaging/session_1/channels/760000000001/messages/951/viewed', }) expect(server.requests[0]?.headers['idempotency-key']).toBe('viewed_951') }) - test('rejects a non-positive message server ID before transport', async () => { + test('rejects an invalid public message ID before transport', async () => { server = await startHttpServer({body: {success: true}}) const result = await apiKeyEnvironment(() => runCommand( - 'channel message mark-viewed 120363@newsletter 0 --session session_1 --json', + 'channel message mark-viewed 760000000001 0 --session session_1 --json', ), ) expect(exitCode(result.error)).toBe(2) expect(result.stderr).toContain( - 'Message server ID must be a positive integer', + 'Message ID must be an opaque decimal string', ) expect(server.requests).toHaveLength(0) }) @@ -235,7 +235,7 @@ describe('channel commands', () => { const result = await apiKeyEnvironment(() => runCommand( - 'channel message react 120363@newsletter 951 --session session_1 --reaction 👍 --idempotency-key reaction_951 --json', + 'channel message react 760000000001 951 --session session_1 --reaction 👍 --idempotency-key reaction_951 --json', ), ) @@ -247,7 +247,7 @@ describe('channel commands', () => { expect(server.requests[0]).toMatchObject({ body: JSON.stringify({reaction: '👍'}), method: 'POST', - url: '/api/session_1/channels/120363%40newsletter/messages/951/reaction', + url: '/messaging/session_1/channels/760000000001/messages/951/reaction', }) }) @@ -256,7 +256,7 @@ describe('channel commands', () => { const result = await apiKeyEnvironment(() => runCommand( - `channel message react 120363@newsletter 951 --session session_1 --reaction ${'👍'.repeat(9)} --json`, + `channel message react 760000000001 951 --session session_1 --reaction ${'👍'.repeat(9)} --json`, ), ) @@ -275,7 +275,7 @@ describe('channel commands', () => { const result = await apiKeyEnvironment(() => runCommand( - 'channel live-updates subscribe 120363@newsletter --session session/main --idempotency-key live_1', + 'channel live-updates subscribe 760000000001 --session session/main --idempotency-key live_1', ), ) @@ -287,7 +287,7 @@ describe('channel commands', () => { expect(server.requests[0]).toMatchObject({ body: '', method: 'POST', - url: '/api/session%2Fmain/channels/120363%40newsletter/live-updates', + url: '/messaging/session%2Fmain/channels/760000000001/live-updates', }) expect(server.requests[0]?.headers['idempotency-key']).toBe('live_1') }) @@ -306,7 +306,7 @@ describe('channel commands', () => { const result = await apiKeyEnvironment(() => runCommand( - `channel ${action} 120363@newsletter --session session_1 --idempotency-key ${action}_1 --json`, + `channel ${action} 760000000001 --session session_1 --idempotency-key ${action}_1 --json`, ), ) @@ -318,7 +318,7 @@ describe('channel commands', () => { expect(server.requests[0]).toMatchObject({ body: '', method: 'POST', - url: `/api/session_1/channels/120363%40newsletter/${action}`, + url: `/messaging/session_1/channels/760000000001/${action}`, }) expect(server.requests[0]?.headers['idempotency-key']).toBe(`${action}_1`) }, @@ -328,7 +328,8 @@ describe('channel commands', () => { if (server === undefined) throw new Error('Test server was not started.') return withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, @@ -353,7 +354,7 @@ function channel() { return { description: 'Product news', followers: 42, - lid: '120363@newsletter', + id: '760000000001', muted: false, name: 'Polymorfa updates', preview: false, @@ -363,9 +364,11 @@ function channel() { function channelMessage() { return { - id: 'message_951', + id: '760000000951', + whatsapp_id: 'message_951', + conversation: {id: '760000000001'}, reactionCounts: {'👍': 2}, - serverId: 951, + position: 951, text: 'A new release is available.', timestamp: '2026-08-20T12:00:00.000Z', type: 'text', @@ -375,9 +378,11 @@ function channelMessage() { function withoutUndefinedText() { return { - id: 'message_951', + id: '760000000951', + whatsapp_id: 'message_951', + conversation: {id: '760000000001'}, reactionCounts: {'👍': 2}, - serverId: 951, + position: 951, timestamp: '2026-08-20T12:00:00.000Z', type: 'text', views: 10, diff --git a/test/commands/contacts.test.ts b/test/commands/contacts.test.ts index 3d8c1b5f..d92e5a1e 100644 --- a/test/commands/contacts.test.ts +++ b/test/commands/contacts.test.ts @@ -30,14 +30,14 @@ describe('contact commands', () => { data: [contact()], metadata: {attempts: 1, requestId: 'req_contacts', status: 200}, }) - expect(server.requests[0]?.url).toBe('/api/session%2Fmain/contacts') + expect(server.requests[0]?.url).toBe('/messaging/session%2Fmain/contacts') }) test('checks multiple phone numbers in one exact registration query', async () => { server = await startHttpServer({ body: { data: [ - {exists: true, lid: 'alice@lid', phoneNumber: '+15551234567'}, + {exists: true, lid: '750000000001', phoneNumber: '+15551234567'}, {exists: false, phoneNumber: '+15557654321'}, ], success: true, @@ -55,7 +55,7 @@ describe('contact commands', () => { data: [{exists: true}, {exists: false}], }) expect(server.requests[0]?.url).toBe( - '/api/session_1/contacts/check?phone=%2B15551234567%2C%2B15557654321', + '/messaging/session_1/contacts/check?phone=%2B15551234567%2C%2B15557654321', ) }) @@ -75,15 +75,15 @@ describe('contact commands', () => { server = await startHttpServer({body: {data: contact(), success: true}}) const result = await apiKeyEnvironment(() => - runCommand('contact view alice@lid --session session/main'), + runCommand('contact view 750000000001 --session session/main'), ) expect(result.error).toBeUndefined() expect(result.stdout).toContain('Alice Example') - expect(result.stdout).toContain('LID\talice@lid') + expect(result.stdout).toContain('ID\t750000000001') expect(result.stdout).toContain('Phone\t+15551234567') expect(server.requests[0]?.url).toBe( - '/api/session%2Fmain/contacts/alice%40lid', + '/messaging/session%2Fmain/contacts/750000000001', ) }) @@ -93,13 +93,13 @@ describe('contact commands', () => { }) const result = await apiKeyEnvironment(() => - runCommand('contact picture alice@lid --session session_1'), + runCommand('contact picture 750000000001 --session session_1'), ) expect(result.error).toBeUndefined() expect(result.stdout).toBe('https://example.com/alice.jpg\n') expect(server.requests[0]?.url).toBe( - '/api/session_1/contacts/alice%40lid/picture', + '/messaging/session_1/contacts/750000000001/picture', ) }) @@ -110,7 +110,7 @@ describe('contact commands', () => { }) const result = await apiKeyEnvironment(() => - runCommand('contact info alice@lid --session session_1 --json'), + runCommand('contact info 750000000001 --session session_1 --json'), ) expect(result.error).toBeUndefined() @@ -119,7 +119,7 @@ describe('contact commands', () => { metadata: {attempts: 1, requestId: 'req_contact_info', status: 200}, }) expect(server.requests[0]?.url).toBe( - '/api/session_1/contacts/alice%40lid/info', + '/messaging/session_1/contacts/750000000001/info', ) }) @@ -132,7 +132,7 @@ describe('contact commands', () => { }) const result = await apiKeyEnvironment(() => - runCommand('contact devices alice@lid --session session_1'), + runCommand('contact devices 750000000001 --session session_1'), ) expect(result.error).toBeUndefined() @@ -140,7 +140,7 @@ describe('contact commands', () => { 'alice:1@s.whatsapp.net\nalice:2@s.whatsapp.net\n', ) expect(server.requests[0]?.url).toBe( - '/api/session_1/contacts/alice%40lid/devices', + '/messaging/session_1/contacts/750000000001/devices', ) }) @@ -150,22 +150,25 @@ describe('contact commands', () => { }) const result = await apiKeyEnvironment(() => - runCommand('contact business-profile alice@lid --session session_1'), + runCommand('contact business-profile 750000000001 --session session_1'), ) expect(result.error).toBeUndefined() - expect(result.stdout).toContain('JID\talice@s.whatsapp.net') + expect(result.stdout).toContain('ID\t750000000001') expect(result.stdout).toContain('Email\talice@example.com') expect(result.stdout).toContain('Category\tSoftware') expect(result.stdout).toContain('Website\thttps://example.com') expect(server.requests[0]?.url).toBe( - '/api/session_1/contacts/alice%40lid/business-profile', + '/messaging/session_1/contacts/750000000001/business-profile', ) }) test('lists exact blocklist hash and JIDs', async () => { server = await startHttpServer({ - body: {data: {hash: 'hash_1', jids: ['blocked@lid']}, success: true}, + body: { + data: {hash: 'hash_1', contacts: [{id: '750000000002'}]}, + success: true, + }, }) const result = await apiKeyEnvironment(() => @@ -174,10 +177,12 @@ describe('contact commands', () => { expect(result.error).toBeUndefined() expect(JSON.parse(result.stdout)).toEqual({ - data: {hash: 'hash_1', jids: ['blocked@lid']}, + data: {hash: 'hash_1', contacts: [{id: '750000000002'}]}, metadata: {attempts: 1, status: 200}, }) - expect(server.requests[0]?.url).toBe('/api/session_1/contacts/blocked') + expect(server.requests[0]?.url).toBe( + '/messaging/session_1/contacts/blocked', + ) }) test.each(['block', 'unblock'] as const)( @@ -189,7 +194,7 @@ describe('contact commands', () => { const result = await apiKeyEnvironment(() => runCommand( - `contact ${action} alice@lid --session session/main --idempotency-key ${action}_alice --json`, + `contact ${action} 750000000001 --session session/main --idempotency-key ${action}_alice --json`, ), ) @@ -201,7 +206,7 @@ describe('contact commands', () => { expect(server.requests[0]).toMatchObject({ body: '', method: 'POST', - url: `/api/session%2Fmain/contacts/alice%40lid/${action}`, + url: `/messaging/session%2Fmain/contacts/750000000001/${action}`, }) expect(server.requests[0]?.headers['idempotency-key']).toBe( `${action}_alice`, @@ -213,7 +218,8 @@ describe('contact commands', () => { if (server === undefined) throw new Error('Test server was not started.') return withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, @@ -237,8 +243,7 @@ describe('contact commands', () => { function contact() { return { businessName: 'Alice Software', - id: 'alice@s.whatsapp.net', - lid: 'alice@lid', + id: '750000000001', name: 'Alice Example', phoneNumber: '+15551234567', profileUrl: 'https://example.com/alice.jpg', @@ -249,10 +254,8 @@ function contact() { function userInfo() { return { - devices: ['alice:1@s.whatsapp.net'], - id: 'alice@s.whatsapp.net', - jid: 'alice@s.whatsapp.net', - lid: 'alice@lid', + devices: [{id: '750000000001', device: 1}], + id: '750000000001', phoneNumber: '+15551234567', pictureId: 'picture_1', status: 'Available', @@ -263,6 +266,7 @@ function userInfo() { function businessProfile() { return { + id: '750000000001', address: '1 Example Street', categories: [{id: 'software', name: 'Software'}], coverPhotoId: 'cover_1', @@ -277,7 +281,6 @@ function businessProfile() { }, ], hoursTimeZone: 'America/New_York', - jid: 'alice@s.whatsapp.net', options: {cartEnabled: 'true'}, websites: ['https://example.com'], } diff --git a/test/commands/customers.test.ts b/test/commands/customers.test.ts index a2799720..3284742d 100644 --- a/test/commands/customers.test.ts +++ b/test/commands/customers.test.ts @@ -53,8 +53,8 @@ describe('Customer commands', () => { expect(status.error).toBeUndefined() expect(enable.error).toBeUndefined() expect(server.requests.map(({method, url}) => `${method} ${url}`)).toEqual([ - 'GET /v1/projects/project%2F1/customers/status', - 'POST /v1/projects/project%2F1/customers/enable', + 'GET /platform/projects/project%2F1/customers/status', + 'POST /platform/projects/project%2F1/customers/enable', ]) expect(server.requests[1]?.headers['idempotency-key']).toBe('enable_1') }) @@ -75,7 +75,7 @@ describe('Customer commands', () => { expect(result.error).toBeUndefined() expect(server.requests[0]?.url).toBe( - '/v1/customers?projectId=project%2F1&cursor=cursor%2F1&limit=20&search=Ada&status=active&isDefault=false&hasNumbers=true&needsAttention=false', + '/platform/customers?projectId=project%2F1&cursor=cursor%2F1&limit=20&search=Ada&status=active&isDefault=false&hasNumbers=true&needsAttention=false', ) expect(JSON.parse(result.stdout)).toMatchObject({ data: { @@ -110,9 +110,9 @@ describe('Customer commands', () => { expect(viewed.error).toBeUndefined() expect(updated.error).toBeUndefined() expect(server.requests.map(({method, url}) => `${method} ${url}`)).toEqual([ - 'POST /v1/customers', - 'GET /v1/customers/customer%2F1?projectId=project%2F1', - 'PATCH /v1/customers/customer%2F1', + 'POST /platform/customers', + 'GET /platform/customers/customer%2F1?projectId=project%2F1', + 'PATCH /platform/customers/customer%2F1', ]) expect(JSON.parse(server.requests[0]?.body ?? '')).toEqual({ externalCustomerId: 'crm/1', @@ -147,8 +147,8 @@ describe('Customer commands', () => { expect(archived.error).toBeUndefined() expect(restored.error).toBeUndefined() expect(server.requests.map(({method, url}) => `${method} ${url}`)).toEqual([ - 'POST /v1/customers/customer%2F1/archive', - 'POST /v1/customers/customer%2F1/restore', + 'POST /platform/customers/customer%2F1/archive', + 'POST /platform/customers/customer%2F1/restore', ]) expect( server.requests.map(({headers}) => headers['idempotency-key']), @@ -180,9 +180,9 @@ describe('Customer commands', () => { expect(events.error).toBeUndefined() expect(links.error).toBeUndefined() expect(server.requests.map(({url}) => url)).toEqual([ - '/v1/customers/customer%2F1/numbers?projectId=project%2F1', - '/v1/customers/customer%2F1/events?projectId=project%2F1&limit=25', - '/v1/customers/customer%2F1/pairing-links?projectId=project%2F1', + '/platform/customers/customer%2F1/numbers?projectId=project%2F1', + '/platform/customers/customer%2F1/events?projectId=project%2F1&limit=25', + '/platform/customers/customer%2F1/pairing-links?projectId=project%2F1', ]) }) @@ -213,9 +213,9 @@ describe('Customer commands', () => { expect(revoked.error).toBeUndefined() expect(transferred.error).toBeUndefined() expect(server.requests.map(({method, url}) => `${method} ${url}`)).toEqual([ - 'POST /v1/customers/customer%2F1/pairing-links', - 'DELETE /v1/customers/customer%2F1/pairing-links/link%2F1?projectId=project%2F1', - 'POST /v1/customers/customer%2F1/numbers/session%2F1/transfer', + 'POST /platform/customers/customer%2F1/pairing-links', + 'DELETE /platform/customers/customer%2F1/pairing-links/link%2F1?projectId=project%2F1', + 'POST /platform/customers/customer%2F1/numbers/session%2F1/transfer', ]) expect(JSON.parse(server.requests[0]?.body ?? '')).toEqual({ expectedPhone: '+15551234567', @@ -256,7 +256,8 @@ describe('Customer commands', () => { if (server === undefined) throw new Error('Test server was not started.') return withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, diff --git a/test/commands/groups.test.ts b/test/commands/groups.test.ts index 8bddb24f..fffde134 100644 --- a/test/commands/groups.test.ts +++ b/test/commands/groups.test.ts @@ -101,7 +101,9 @@ describe('group commands', () => { expect(JSON.parse(result.stdout)).toMatchObject({ data: {id: '123@g.us', name: 'Launch team'}, }) - expect(server.requests[0]?.url).toBe('/api/session_1/groups/123%40g.us') + expect(server.requests[0]?.url).toBe( + '/messaging/session_1/groups/123%40g.us', + ) }) test('lists participants without claiming pagination', async () => { @@ -146,7 +148,8 @@ describe('group commands', () => { if (server === undefined) throw new Error('Test server was not started.') return withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, diff --git a/test/commands/labels.test.ts b/test/commands/labels.test.ts index 4895c3f4..6b8ebf43 100644 --- a/test/commands/labels.test.ts +++ b/test/commands/labels.test.ts @@ -30,7 +30,7 @@ describe('label commands', () => { data: [label()], metadata: {attempts: 1, requestId: 'req_labels', status: 200}, }) - expect(server.requests[0]?.url).toBe('/api/session%2Fmain/labels') + expect(server.requests[0]?.url).toBe('/messaging/session%2Fmain/labels') }) test('preserves the observation envelope only when requested', async () => { @@ -52,7 +52,7 @@ describe('label commands', () => { expect(result.error).toBeUndefined() expect(JSON.parse(result.stdout)).toMatchObject({data: collection}) expect(server.requests[0]?.url).toBe( - '/api/session_1/labels?includeObservation=true', + '/messaging/session_1/labels?includeObservation=true', ) }) @@ -88,7 +88,7 @@ describe('label commands', () => { expect(JSON.parse(result.stdout)).toMatchObject({data: label()}) expect(server.requests[0]).toMatchObject({ method: 'POST', - url: '/api/session_1/labels', + url: '/messaging/session_1/labels', }) expect(JSON.parse(server.requests[0]?.body ?? '')).toEqual({ color: 5, @@ -123,7 +123,7 @@ describe('label commands', () => { expect(result.error).toBeUndefined() expect(server.requests[0]).toMatchObject({ method: 'PUT', - url: '/api/session_1/labels/label%2F1', + url: '/messaging/session_1/labels/label%2F1', }) expect(JSON.parse(server.requests[0]?.body ?? '')).toEqual({ color: 7, @@ -153,7 +153,7 @@ describe('label commands', () => { expect(server.requests[0]).toMatchObject({ body: '', method: 'DELETE', - url: '/api/session_1/labels/label%2F1', + url: '/messaging/session_1/labels/label%2F1', }) expect(server.requests[0]?.headers['idempotency-key']).toBe('delete_1') }) @@ -169,7 +169,7 @@ describe('label commands', () => { expect(result.error).toBeUndefined() expect(server.requests[0]?.url).toBe( - '/api/session%2Fmain/labels/chats/120363%40g.us', + '/messaging/session%2Fmain/labels/chats/120363%40g.us', ) }) @@ -186,7 +186,7 @@ describe('label commands', () => { expect(server.requests[0]).toMatchObject({ body: JSON.stringify({labels: ['priority', 'customer']}), method: 'PUT', - url: '/api/session_1/labels/chats/120363%40g.us', + url: '/messaging/session_1/labels/chats/120363%40g.us', }) expect(server.requests[0]?.headers['idempotency-key']).toBe('replace_1') }) @@ -220,7 +220,8 @@ describe('label commands', () => { if (server === undefined) throw new Error('Test server was not started.') return withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, diff --git a/test/commands/media.test.ts b/test/commands/media.test.ts index c3c1f2a1..e22160ca 100644 --- a/test/commands/media.test.ts +++ b/test/commands/media.test.ts @@ -41,7 +41,7 @@ describe('media commands', () => { data: mediaInfo(), metadata: {attempts: 1, requestId: 'req_media_info', status: 200}, }) - expect(server.requests[0]?.url).toBe('/api/media/media%2F1/info') + expect(server.requests[0]?.url).toBe('/messaging/media/media%2F1/info') }) test('rejects a client token before a Messaging media request', async () => { @@ -98,8 +98,8 @@ describe('media commands', () => { Buffer.from([0x89, 0x50, 0x4e, 0x47]), ) expect(server.requests.map(({url}) => url)).toEqual([ - '/api/media/media%2F1/info', - '/api/media/media%2F1', + '/messaging/media/media%2F1/info', + '/messaging/media/media%2F1', ]) }) @@ -196,7 +196,7 @@ describe('media commands', () => { expect(server.requests[0]).toMatchObject({ body: '', method: 'POST', - url: '/api/media/media%2F1/download-and-save', + url: '/messaging/media/media%2F1/download-and-save', }) expect(server.requests[0]?.headers['idempotency-key']).toBe('persist_1') }) @@ -205,7 +205,8 @@ describe('media commands', () => { if (server === undefined) throw new Error('Test server was not started.') return withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, diff --git a/test/commands/messages.test.ts b/test/commands/messages.test.ts index 4714f214..4b0b3cc2 100644 --- a/test/commands/messages.test.ts +++ b/test/commands/messages.test.ts @@ -20,7 +20,7 @@ describe('message commands', () => { const result = await apiKeyEnvironment(() => runCommand( - 'message send text --session session/main --to 15551234567@s.whatsapp.net --body Hello --json', + 'message send text --session session/main --to +15551234567 --body Hello --json', ), ) @@ -31,12 +31,11 @@ describe('message commands', () => { }) expect(server.requests[0]).toMatchObject({ body: JSON.stringify({ - chatId: '15551234567@s.whatsapp.net', - text: 'Hello', - type: 'text', + conversation: {phoneNumber: '+15551234567'}, + content: {text: 'Hello'}, }), method: 'POST', - url: '/api/session%2Fmain/messages/send', + url: '/messaging/session%2Fmain/messages/send', }) }) @@ -45,26 +44,25 @@ describe('message commands', () => { const result = await apiKeyEnvironment(() => runCommand( - 'message send text --session session_1 --to 120363@g.us --body Reply --reply-to message_1 --reply-participant member@lid --forwarded --mention first@lid --mention second@lid --idempotency-key send_1 --json', + 'message send text --session session_1 --to 730000000001 --body Reply --reply-to 740000000001 --forwarded --mention 750000000001 --mention 750000000002 --idempotency-key send_1 --json', ), ) expect(result.error).toBeUndefined() expect(JSON.parse(server.requests[0]?.body ?? '')).toEqual({ - chatId: '120363@g.us', + conversation: {id: '730000000001'}, + content: {text: 'Reply'}, isForwarded: true, - mentions: ['first@lid', 'second@lid'], - quotedMessage: {messageId: 'message_1', participant: 'member@lid'}, - text: 'Reply', - type: 'text', + mentions: ['750000000001', '750000000002'], + quotedMessage: {id: '740000000001'}, }) expect(server.requests[0]?.headers['idempotency-key']).toBe('send_1') - expect(server.requests[0]?.url).toBe('/api/session_1/messages/send') + expect(server.requests[0]?.url).toBe('/messaging/session_1/messages/send') }) test('requires explicit session scope before sending', async () => { const result = await runCommand( - 'message send text --to 15551234567@s.whatsapp.net --body Hello --json', + 'message send text --to +15551234567 --body Hello --json', ) expect(exitCode(result.error)).toBe(2) @@ -77,7 +75,7 @@ describe('message commands', () => { const result = await apiKeyEnvironment(() => runCommand( - 'message send text --session session_1 --to 15551234567@s.whatsapp.net --body "" --json', + 'message send text --session session_1 --to +15551234567 --body "" --json', ), ) @@ -98,7 +96,7 @@ describe('message commands', () => { }, () => runCommand( - 'message send text --session session_1 --to 15551234567@s.whatsapp.net --body Hello --json', + 'message send text --session session_1 --to +15551234567 --body Hello --json', ), ) @@ -111,7 +109,7 @@ describe('message commands', () => { const result = await apiKeyEnvironment(() => runCommand( - 'message react message_1 --session session/main --chat 120363@g.us --emoji 👍 --idempotency-key react_1 --json', + 'message react 740000000001 --session session/main --chat 730000000001 --emoji 👍 --idempotency-key react_1 --json', ), ) @@ -122,12 +120,12 @@ describe('message commands', () => { }) expect(server.requests[0]).toMatchObject({ body: JSON.stringify({ - chatId: '120363@g.us', - messageId: 'message_1', + conversation: {id: '730000000001'}, + id: '740000000001', reaction: '👍', }), method: 'POST', - url: '/api/session%2Fmain/messages/react', + url: '/messaging/session%2Fmain/messages/react', }) expect(server.requests[0]?.headers['idempotency-key']).toBe('react_1') }) @@ -137,7 +135,7 @@ describe('message commands', () => { const result = await apiKeyEnvironment(() => runCommand( - 'message mark-seen message_1 --session session_1 --chat 120363@g.us --json', + 'message mark-seen 740000000001 --session session_1 --chat 730000000001 --json', ), ) @@ -147,9 +145,12 @@ describe('message commands', () => { metadata: {attempts: 1, status: 200}, }) expect(server.requests[0]).toMatchObject({ - body: JSON.stringify({chatId: '120363@g.us', messageId: 'message_1'}), + body: JSON.stringify({ + conversation: {id: '730000000001'}, + id: '740000000001', + }), method: 'POST', - url: '/api/session_1/messages/seen', + url: '/messaging/session_1/messages/seen', }) }) @@ -158,40 +159,45 @@ describe('message commands', () => { const result = await apiKeyEnvironment(() => runCommand( - 'message typing --session session_1 --chat 120363@g.us --state recording --json', + 'message typing --session session_1 --chat 730000000001 --state recording --json', ), ) expect(result.error).toBeUndefined() expect(JSON.parse(result.stdout)).toMatchObject({data: {success: true}}) expect(server.requests[0]).toMatchObject({ - body: JSON.stringify({chatId: '120363@g.us', state: 'recording'}), + body: JSON.stringify({ + conversation: {id: '730000000001'}, + state: 'recording', + }), method: 'POST', - url: '/api/session_1/messages/typing', + url: '/messaging/session_1/messages/typing', }) }) test('removes a star through the explicit remove flag', async () => { - server = await messageServer() + server = await startHttpServer({ + body: {success: true, data: {status: 'OK'}}, + }) const result = await apiKeyEnvironment(() => runCommand( - 'message star message_1 --session session_1 --chat 120363@g.us --remove --json', + 'message star 740000000001 --session session_1 --chat 730000000001 --remove --json', ), ) expect(result.error).toBeUndefined() expect(JSON.parse(result.stdout)).toMatchObject({ - data: {id: 'message_1', status: 'sent'}, + data: {status: 'OK'}, }) expect(server.requests[0]).toMatchObject({ body: JSON.stringify({ - chatId: '120363@g.us', - messageId: 'message_1', + conversation: {id: '730000000001'}, + id: '740000000001', star: false, }), method: 'POST', - url: '/api/session_1/messages/star', + url: '/messaging/session_1/messages/star', }) }) @@ -199,7 +205,8 @@ describe('message commands', () => { if (server === undefined) throw new Error('Test server was not started.') return withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, @@ -217,12 +224,10 @@ async function messageServer(requestId?: string): Promise { function completeMessage() { return { - fromLid: 'sender@lid', - fromPhoneNumber: '15550000000', - id: 'message_1', - senderLid: 'sender@lid', - senderPhoneNumber: '15550000000', - status: 'sent', + id: '740000000001', + whatsapp_id: '3A000000000000000001', + conversation: {id: '730000000001', phoneNumber: '+15551234567'}, + type: 'text', timestamp: '2026-08-19T20:00:00.000Z', } } diff --git a/test/commands/platform-management.test.ts b/test/commands/platform-management.test.ts index 81f96a4b..893d366c 100644 --- a/test/commands/platform-management.test.ts +++ b/test/commands/platform-management.test.ts @@ -55,7 +55,10 @@ describe('Platform management commands', () => { data: [{keyId: 'key_1', label: 'Production', last4: '1234'}], metadata: {attempts: 1, requestId: 'req_keys', status: 200}, }) - expect(server.requests[0]).toMatchObject({method: 'GET', url: '/v1/keys'}) + expect(server.requests[0]).toMatchObject({ + method: 'GET', + url: '/platform/keys', + }) }) test('revokes one key only with confirmation and preserves an idempotency key', async () => { @@ -69,7 +72,7 @@ describe('Platform management commands', () => { expect(server.requests[0]).toMatchObject({ body: '', method: 'DELETE', - url: '/v1/keys/key%2F1', + url: '/platform/keys/key%2F1', }) expect(server.requests[0]?.headers['idempotency-key']).toBe('revoke_1') }) @@ -111,7 +114,7 @@ describe('Platform management commands', () => { ) expect(result.error).toBeUndefined() - expect(server.requests[0]?.url).toBe('/v1/members') + expect(server.requests[0]?.url).toBe('/platform/members') }) test('lists audit logs with exact filters and a bounded limit', async () => { @@ -125,7 +128,7 @@ describe('Platform management commands', () => { expect(result.error).toBeUndefined() expect(server.requests[0]?.url).toBe( - '/v1/audit?action=key.revoked&resource=api-key&limit=500', + '/platform/audit?action=key.revoked&resource=api-key&limit=500', ) }) @@ -144,8 +147,11 @@ describe('Platform management commands', () => { }) test.each([ - {command: 'session-ban list --json', path: '/v1/bans'}, - {command: 'session-ban list --active --json', path: '/v1/bans/active'}, + {command: 'session-ban list --json', path: '/platform/bans'}, + { + command: 'session-ban list --active --json', + path: '/platform/bans/active', + }, ])('lists session bans from $path', async ({command, path}) => { server = await startHttpServer({body: {data: []}}) @@ -171,7 +177,7 @@ describe('Platform management commands', () => { expect(server.requests[0]).toMatchObject({ body: '', method: 'POST', - url: '/v1/incidents/incident%2F1/acknowledge', + url: '/platform/incidents/incident%2F1/acknowledge', }) expect(server.requests[0]?.headers['idempotency-key']).toBe('ack_1') }) @@ -184,7 +190,7 @@ describe('Platform management commands', () => { ) expect(result.error).toBeUndefined() - expect(server.requests[0]?.url).toBe('/v1/incidents') + expect(server.requests[0]?.url).toBe('/platform/incidents') }) test('retrieves one durable Platform operation without claiming watch semantics', async () => { @@ -193,13 +199,12 @@ describe('Platform management commands', () => { data: { completedAt: null, createdAt: '2026-08-20T00:00:00Z', - failureCode: null, + error: null, id: 'operation/1', kind: 'session.stop', - progressCode: null, + progress: null, projectId: 'project_1', - resourceId: 'session_1', - resourceType: 'session', + resource: {id: 'session_1', type: 'session'}, status: 'running', updatedAt: '2026-08-20T00:00:01Z', }, @@ -210,8 +215,8 @@ describe('Platform management commands', () => { runCommand('operation view operation/1 --json'), ) - expect(result.error).toBeUndefined() - expect(server.requests[0]?.url).toBe('/v1/operations/operation%2F1') + expect(result.error).toBeDefined() + expect(server.requests).toEqual([]) }) test('lists project-token metadata for one explicit project', async () => { @@ -222,7 +227,9 @@ describe('Platform management commands', () => { ) expect(result.error).toBeUndefined() - expect(server.requests[0]?.url).toBe('/v1/tokens?projectId=project%2F1') + expect(server.requests[0]?.url).toBe( + '/platform/tokens?projectId=project%2F1', + ) }) test('requires a project for project-token metadata before transport', async () => { @@ -256,7 +263,10 @@ describe('Platform management commands', () => { status: 200, }, }) - expect(server.requests[0]).toMatchObject({method: 'GET', url: '/v1/widget'}) + expect(server.requests[0]).toMatchObject({ + method: 'GET', + url: '/platform/quicklink', + }) }) test('retrieves project widget settings through an explicit encoded target', async () => { @@ -267,15 +277,17 @@ describe('Platform management commands', () => { ) expect(result.error).toBeUndefined() - expect(server.requests[0]?.url).toBe('/v1/widget?projectId=project%2F1') + expect(server.requests[0]?.url).toBe( + '/platform/quicklink?projectId=project%2F1', + ) }) test('updates a widget patch with separate project scope and idempotency', async () => { const input = await jsonInput('widget.json', { - allowedOrigins: ['https://app.example.com'], + allowedRedirectUris: ['https://app.example.com/connected'], businessName: 'Example', enabled: true, - modesAllowed: ['embedded'], + methods: ['qr'], theme: 'dark', }) server = await startHttpServer({body: {data: widgetSettings()}}) @@ -288,14 +300,17 @@ describe('Platform management commands', () => { expect(result.error).toBeUndefined() expect(JSON.parse(server.requests[0]?.body ?? '')).toEqual({ - allowedOrigins: ['https://app.example.com'], + allowedRedirectUris: ['https://app.example.com/connected'], businessName: 'Example', enabled: true, - modesAllowed: ['embedded'], + methods: ['qr'], projectId: 'project_1', theme: 'dark', }) - expect(server.requests[0]).toMatchObject({method: 'PUT', url: '/v1/widget'}) + expect(server.requests[0]).toMatchObject({ + method: 'PUT', + url: '/platform/quicklink', + }) expect(server.requests[0]?.headers['idempotency-key']).toBe('widget_1') }) @@ -335,7 +350,7 @@ describe('Platform management commands', () => { }) expect(server.requests[0]).toMatchObject({ method: 'POST', - url: '/v1/sessions/stop', + url: '/platform/sessions/stop', }) expect(server.requests[0]?.headers['idempotency-key']).toBe('stop_batch') }) @@ -379,7 +394,7 @@ describe('Platform management commands', () => { projectId: 'project_1', sessionIds: ['session_1', 'session_2'], }) - expect(server.requests[0]?.url).toBe('/v1/sessions/delete') + expect(server.requests[0]?.url).toBe('/platform/sessions/delete') expect(server.requests[0]?.headers['idempotency-key']).toBe('delete_batch') }) @@ -406,7 +421,8 @@ describe('Platform management commands', () => { if (server === undefined) throw new Error('Test server was not started.') return withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, diff --git a/test/commands/presence.test.ts b/test/commands/presence.test.ts index 9f4e3e48..d005f98d 100644 --- a/test/commands/presence.test.ts +++ b/test/commands/presence.test.ts @@ -30,7 +30,7 @@ describe('presence commands', () => { expect(result.stdout).toContain('Last sent\tavailable') expect(result.stdout).toContain('Authoritative\tno') expect(result.stdout).not.toContain('live') - expect(server.requests[0]?.url).toBe('/api/session%2Fmain/presence') + expect(server.requests[0]?.url).toBe('/messaging/session%2Fmain/presence') }) test('preserves self presence and response metadata as JSON', async () => { @@ -69,7 +69,7 @@ describe('presence commands', () => { expect(server.requests[0]).toMatchObject({ body: JSON.stringify({presence: 'unavailable'}), method: 'POST', - url: '/api/session_1/presence', + url: '/messaging/session_1/presence', }) expect(server.requests[0]?.headers['idempotency-key']).toBe('presence_1') }) @@ -122,7 +122,7 @@ describe('presence commands', () => { }, }) expect(server.requests[0]?.url).toBe( - '/api/session%2Fmain/presence/120363%40g.us', + '/messaging/session%2Fmain/presence/120363%40g.us', ) }) @@ -172,7 +172,7 @@ describe('presence commands', () => { expect(server.requests[0]).toMatchObject({ body: '', method: 'POST', - url: '/api/session%2Fmain/presence/member%40lid/subscribe', + url: '/messaging/session%2Fmain/presence/member%40lid/subscribe', }) expect(server.requests[0]?.headers['idempotency-key']).toBe( 'presence_sub_1', @@ -183,7 +183,8 @@ describe('presence commands', () => { if (server === undefined) throw new Error('Test server was not started.') return withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, diff --git a/test/commands/privacy.test.ts b/test/commands/privacy.test.ts index 8611d33b..12f44985 100644 --- a/test/commands/privacy.test.ts +++ b/test/commands/privacy.test.ts @@ -30,7 +30,7 @@ describe('privacy commands', () => { data: privacySettings(), metadata: {attempts: 1, requestId: 'req_privacy', status: 200}, }) - expect(server.requests[0]?.url).toBe('/api/session%2Fmain/privacy') + expect(server.requests[0]?.url).toBe('/messaging/session%2Fmain/privacy') }) test('renders privacy settings as a readable setting matrix', async () => { @@ -88,7 +88,7 @@ describe('privacy commands', () => { expect(server.requests[0]).toMatchObject({ body: JSON.stringify({value: 'match_last_seen'}), method: 'PUT', - url: '/api/session_1/privacy/online', + url: '/messaging/session_1/privacy/online', }) expect(server.requests[0]?.headers['idempotency-key']).toBe( 'privacy_online_1', @@ -152,7 +152,7 @@ describe('privacy commands', () => { expect(server.requests[0]).toMatchObject({ body: JSON.stringify({durationSeconds}), method: 'PUT', - url: '/api/session_1/privacy/disappearing/default', + url: '/messaging/session_1/privacy/disappearing/default', }) expect(server.requests[0]?.headers['idempotency-key']).toBe( `timer_${duration}`, @@ -164,7 +164,8 @@ describe('privacy commands', () => { if (server === undefined) throw new Error('Test server was not started.') return withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, diff --git a/test/commands/profile.test.ts b/test/commands/profile.test.ts index 8388f0e9..5f51e2b8 100644 --- a/test/commands/profile.test.ts +++ b/test/commands/profile.test.ts @@ -41,7 +41,7 @@ describe('profile commands', () => { data: declaredProfile(), metadata: {attempts: 1, requestId: 'req_profile', status: 200}, }) - expect(server.requests[0]?.url).toBe('/api/session%2Fmain/profile') + expect(server.requests[0]?.url).toBe('/messaging/session%2Fmain/profile') }) test('renders the live runner profile shape without assuming declared fields', async () => { @@ -118,7 +118,7 @@ describe('profile commands', () => { expect(server.requests[0]).toMatchObject({ body: JSON.stringify({name: 'Polymorfa'}), method: 'PUT', - url: '/api/session_1/profile/name', + url: '/messaging/session_1/profile/name', }) expect(server.requests[0]?.headers['idempotency-key']).toBe('name_1') }) @@ -138,7 +138,7 @@ describe('profile commands', () => { expect(server.requests[0]).toMatchObject({ body: JSON.stringify({status: 'Available'}), method: 'PUT', - url: '/api/session_1/profile/status', + url: '/messaging/session_1/profile/status', }) }) @@ -157,7 +157,7 @@ describe('profile commands', () => { expect(server.requests[0]).toMatchObject({ body: JSON.stringify({url: 'https://example.test/profile.jpg'}), method: 'PUT', - url: '/api/session_1/profile/picture', + url: '/messaging/session_1/profile/picture', }) }) @@ -178,7 +178,7 @@ describe('profile commands', () => { expect(server.requests[0]).toMatchObject({ body: JSON.stringify({base64: '/9j/2Q=='}), method: 'PUT', - url: '/api/session%2Fmain/profile/picture', + url: '/messaging/session%2Fmain/profile/picture', }) expect(server.requests[0]?.headers['idempotency-key']).toBe('picture_1') }) @@ -256,7 +256,7 @@ describe('profile commands', () => { expect(server.requests[0]).toMatchObject({ body: '', method: 'DELETE', - url: '/api/session_1/profile/picture', + url: '/messaging/session_1/profile/picture', }) expect(server.requests[0]?.headers['idempotency-key']).toBe( 'picture_delete_1', @@ -267,7 +267,8 @@ describe('profile commands', () => { if (server === undefined) throw new Error('Test server was not started.') return withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, diff --git a/test/commands/quick-replies.test.ts b/test/commands/quick-replies.test.ts index 528d84ab..13e0ca80 100644 --- a/test/commands/quick-replies.test.ts +++ b/test/commands/quick-replies.test.ts @@ -41,7 +41,7 @@ describe('quick-reply commands', () => { }, }) expect(server.requests[0]?.url).toBe( - '/api/session%2Fmain/business/quick-replies', + '/messaging/session%2Fmain/business/quick-replies', ) }) @@ -97,7 +97,7 @@ describe('quick-reply commands', () => { expect(JSON.parse(result.stdout)).toMatchObject({data: quickReply()}) expect(server.requests[0]).toMatchObject({ method: 'POST', - url: '/api/session_1/business/quick-replies', + url: '/messaging/session_1/business/quick-replies', }) expect(JSON.parse(server.requests[0]?.body ?? '')).toEqual({ count: 2, @@ -142,7 +142,7 @@ describe('quick-reply commands', () => { expect(result.error).toBeUndefined() expect(server.requests[0]).toMatchObject({ method: 'PUT', - url: '/api/session%2Fmain/business/quick-replies/reply%2F1', + url: '/messaging/session%2Fmain/business/quick-replies/reply%2F1', }) expect(JSON.parse(server.requests[0]?.body ?? '')).toEqual({ keywords: ['hours'], @@ -175,7 +175,7 @@ describe('quick-reply commands', () => { expect(server.requests[0]).toMatchObject({ body: '', method: 'DELETE', - url: '/api/session_1/business/quick-replies/reply%2F1', + url: '/messaging/session_1/business/quick-replies/reply%2F1', }) expect(server.requests[0]?.headers['idempotency-key']).toBe( 'delete_reply_1', @@ -186,7 +186,8 @@ describe('quick-reply commands', () => { if (server === undefined) throw new Error('Test server was not started.') return withEnvironment( { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: server.baseUrl, POLYMORFA_CLIENT_TOKEN: undefined, }, diff --git a/test/context/environment.test.ts b/test/context/environment.test.ts index fd49ff54..7da99747 100644 --- a/test/context/environment.test.ts +++ b/test/context/environment.test.ts @@ -12,7 +12,8 @@ describe('resolveEnvironment', () => { expect( resolveEnvironment({ env: { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_BASE_URL: 'http://127.0.0.1:4567', UNRELATED_SECRET: 'do-not-copy', }, @@ -21,7 +22,11 @@ describe('resolveEnvironment', () => { }), ).toEqual({ surface: 'platform', - credential: {type: 'apiKey', value: 'pmfa_test-key'}, + credential: { + type: 'apiKey', + value: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, baseUrl: 'http://127.0.0.1:4567', timeoutMs: 5000, }) @@ -51,12 +56,19 @@ describe('resolveEnvironment', () => { test('resolves a server API key for the Messaging surface', () => { expect( resolveEnvironment({ - env: {POLYMORFA_API_KEY: 'pmfa_test-key'}, + env: { + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, surface: 'messaging', }), ).toEqual({ surface: 'messaging', - credential: {type: 'apiKey', value: 'pmfa_test-key'}, + credential: { + type: 'apiKey', + value: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, }) }) @@ -64,7 +76,8 @@ describe('resolveEnvironment', () => { expect(() => resolveEnvironment({ env: { - POLYMORFA_API_KEY: 'pmfa_test-key', + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', POLYMORFA_CLIENT_TOKEN: 'pmfa_ct_browser', }, surface: 'messaging', diff --git a/test/input/stdin.test.ts b/test/input/stdin.test.ts index 7311d5c5..42eb6d72 100644 --- a/test/input/stdin.test.ts +++ b/test/input/stdin.test.ts @@ -6,8 +6,14 @@ import {readSecretFromStdin} from '../../src/input/stdin.js' describe('readSecretFromStdin', () => { test('reads one trimmed secret without returning extra input', async () => { await expect( - readSecretFromStdin(Readable.from([' pmfa_test-key\n'])), - ).resolves.toBe('pmfa_test-key') + readSecretFromStdin( + Readable.from([ + ' pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n', + ]), + ), + ).resolves.toBe( + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + ) }) test('rejects empty and oversized input', async () => { diff --git a/test/sdk/client.test.ts b/test/sdk/client.test.ts index e610870b..410da982 100644 --- a/test/sdk/client.test.ts +++ b/test/sdk/client.test.ts @@ -1,7 +1,7 @@ import { isEvent, MessagingClient, - PlatformClient, + Client as PlatformClient, RawClient, type WebhookEvent, } from '@polymorfa/sdk' @@ -17,7 +17,11 @@ describe('createRawClient', () => { test('returns the public Platform raw client', () => { const client = createRawClient({ surface: 'platform', - credential: {type: 'apiKey', value: 'pmfa_test-key'}, + credential: { + type: 'apiKey', + value: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, }) expect(client).toBeInstanceOf(RawClient) @@ -38,26 +42,36 @@ describe('createRawClient', () => { surface: 'platform', credential: {type: 'clientToken', value: 'pmfa_ct_browser'}, }), - ).toThrow('Platform server API key must use the pmfa_ prefix.') + ).toThrow( + 'Client and project tokens cannot be used as organization server API keys.', + ) }) test('creates a typed Messaging client with session pairing and operation polling', () => { const client = createMessagingClient({ surface: 'messaging', - credential: {type: 'apiKey', value: 'pmfa_test-key'}, + credential: { + type: 'apiKey', + value: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, timeoutMs: 1234, }) expect(client).toBeInstanceOf(MessagingClient) expect(client.sessions).toHaveProperty('qr') expect(client.sessions).toHaveProperty('requestPairingCode') - expect(client.operations).toHaveProperty('retrieve') + expect(client).not.toHaveProperty('operations') }) test('creates a typed Platform client and rejects the wrong surface', () => { const client = createPlatformClient({ surface: 'platform', - credential: {type: 'apiKey', value: 'pmfa_test-key'}, + credential: { + type: 'apiKey', + value: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, }) expect(client).toBeInstanceOf(PlatformClient) @@ -71,7 +85,11 @@ describe('createRawClient', () => { expect(() => createPlatformClient({ surface: 'messaging', - credential: {type: 'apiKey', value: 'pmfa_test-key'}, + credential: { + type: 'apiKey', + value: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, }), ).toThrow('requires the Platform API surface') }) diff --git a/test/services/auth.test.ts b/test/services/auth.test.ts index 9df04f34..f1b58813 100644 --- a/test/services/auth.test.ts +++ b/test/services/auth.test.ts @@ -9,7 +9,8 @@ describe('AuthService', () => { const fixture = createFixture() const result = await fixture.service.login({ - apiKey: 'pmfa_test-key', + apiKey: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', profile: 'work', }) @@ -20,7 +21,7 @@ describe('AuthService', () => { }) expect(fixture.credentials.set).toHaveBeenCalledWith( 'profile:work:apiKey', - 'pmfa_test-key', + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', ) expect(fixture.saved()).toEqual({ activeProfile: 'work', @@ -31,14 +32,20 @@ describe('AuthService', () => { }, version: 1, }) - expect(JSON.stringify(fixture.saved())).not.toContain('pmfa_test-key') + expect(JSON.stringify(fixture.saved())).not.toContain( + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + ) }) test('rejects an invalid profile name before touching secure storage', async () => { const fixture = createFixture() await expect( - fixture.service.login({apiKey: 'pmfa_test-key', profile: '../work'}), + fixture.service.login({ + apiKey: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + profile: '../work', + }), ).rejects.toThrow('Profile names may contain') expect(fixture.credentials.set).not.toHaveBeenCalled() }) @@ -48,7 +55,7 @@ describe('AuthService', () => { await expect( fixture.service.login({apiKey: 'secret', profile: 'work'}), - ).rejects.toThrow('must use the pmfa_ prefix') + ).rejects.toThrow('must use the canonical pmfa_ v1 format') expect(fixture.credentials.set).not.toHaveBeenCalled() }) @@ -87,7 +94,9 @@ describe('AuthService', () => { test('switches the active profile and reports secure credential availability', async () => { const fixture = createFixture(profileConfig()) - vi.mocked(fixture.credentials.get).mockResolvedValue('pmfa_test-key') + vi.mocked(fixture.credentials.get).mockResolvedValue( + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + ) await expect(fixture.service.use('work')).resolves.toMatchObject({ active: true, diff --git a/test/services/groups.test.ts b/test/services/groups.test.ts index 275737d1..2d3dc7f1 100644 --- a/test/services/groups.test.ts +++ b/test/services/groups.test.ts @@ -30,7 +30,7 @@ describe('group read services', () => { data: [group], metadata: {attempts: 1, requestId: 'req_groups', status: 200}, }) - expect(server.requests[0]?.url).toBe('/api/session%2Fmain/groups') + expect(server.requests[0]?.url).toBe('/messaging/session%2Fmain/groups') }) test('retrieves a group and its participants through encoded identifiers', async () => { @@ -41,7 +41,7 @@ describe('group read services', () => { viewGroup(client(server.baseUrl), 'session/main', '123@g.us'), ).resolves.toMatchObject({data: group}) expect(server.requests[0]?.url).toBe( - '/api/session%2Fmain/groups/123%40g.us', + '/messaging/session%2Fmain/groups/123%40g.us', ) await server.close() @@ -72,7 +72,11 @@ describe('group read services', () => { function client(baseUrl: string) { return createMessagingClient({ baseUrl, - credential: {type: 'apiKey', value: 'pmfa_test-key'}, + credential: { + type: 'apiKey', + value: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, surface: 'messaging', }) } From 02abe2c683ba972ac73f139b9d954fa40bd9ea28 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Mon, 14 Sep 2026 03:21:54 +0300 Subject: [PATCH 2/5] fix: finalize published SDK pin and identity guidance --- README.md | 7 ++----- package-lock.json | 6 +++--- package.json | 2 +- src/services/session-identities.ts | 7 +++++-- test/commands/calls-and-identities.test.ts | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 74801de6..94e21608 100644 --- a/README.md +++ b/README.md @@ -572,11 +572,8 @@ polymorfa campaign requeue campaign_123 \ ``` Launch, pause, resume, and stop return accepted durable work; they do not wait -for the state transition. Poll the returned operation explicitly: - -```shell - -``` +for the state transition. Check the campaign with `polymorfa campaign view`. +Operation inspection is console-only; the CLI has no operation-polling command. Launch times accept epoch milliseconds or timezone-qualified ISO 8601 instants. List returns one complete newest-first array with no cursor. Requeue diff --git a/package-lock.json b/package-lock.json index 84027828..7a946fd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@napi-rs/keyring": "1.3.0", "@oclif/core": "^4.13.5", - "@polymorfa/sdk": "github:polymorfa/sdks#31467bdc4e06cc962e8d06f14dd98f4f365b367b" + "@polymorfa/sdk": "github:polymorfa/sdks#93223606d8bf1be9d462b432fc4992d2f71c2b36" }, "bin": { "polymorfa": "bin/run.js" @@ -972,8 +972,8 @@ }, "node_modules/@polymorfa/sdk": { "version": "0.1.0-dev.0", - "resolved": "git+ssh://git@github.com/polymorfa/sdks.git#31467bdc4e06cc962e8d06f14dd98f4f365b367b", - "integrity": "sha512-F7MJsEXMfxo/cyW7u1b6oVOJsG9JOIilEpgf+xVIsP35216lSmbiIOYZ6mTDWiUVukrmGm/yoQvC4mAfiMY8EA==", + "resolved": "git+ssh://git@github.com/polymorfa/sdks.git#93223606d8bf1be9d462b432fc4992d2f71c2b36", + "integrity": "sha512-yb7VRB7xxdvRL5ooBhJdNchgtCWzCl8mZLpHGQkRsPXuJNAX3g8H7T2u1LdArd44vPKDb8gbVZqNja6sO+Dhyw==", "workspaces": [ "packages/calls", "packages/browser", diff --git a/package.json b/package.json index cbbb2040..b5095105 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "dependencies": { "@napi-rs/keyring": "1.3.0", "@oclif/core": "^4.13.5", - "@polymorfa/sdk": "github:polymorfa/sdks#31467bdc4e06cc962e8d06f14dd98f4f365b367b" + "@polymorfa/sdk": "github:polymorfa/sdks#93223606d8bf1be9d462b432fc4992d2f71c2b36" }, "devDependencies": { "@eslint/js": "^9.39.5", diff --git a/src/services/session-identities.ts b/src/services/session-identities.ts index d72de311..948b8b46 100644 --- a/src/services/session-identities.ts +++ b/src/services/session-identities.ts @@ -61,7 +61,10 @@ export async function resolveIdentity( readonly usernameKey?: string }, ): Promise> { - const response = await client.identities.resolve(session, lidParams(input)) + const response = await client.identities.resolve( + session, + identityParams(input), + ) return result(response.data.data, response.metadata) } @@ -110,7 +113,7 @@ export function renderCallRejection(response: RejectCallResponse): string { return 'Rejected the incoming call.' } -function lidParams(input: { +function identityParams(input: { readonly id?: string readonly phoneNumber?: string readonly username?: string diff --git a/test/commands/calls-and-identities.test.ts b/test/commands/calls-and-identities.test.ts index 337e3c28..ed0c8bee 100644 --- a/test/commands/calls-and-identities.test.ts +++ b/test/commands/calls-and-identities.test.ts @@ -102,7 +102,7 @@ describe('call and identity commands', () => { ) }) - test('requires exactly one valid LID identity input before transport', async () => { + test('requires exactly one valid identity input before transport', async () => { server = await startHttpServer({body: {data: {}, success: true}}) const result = await apiKeyEnvironment(() => From 4f5c40f87cca215bb2945bd1791f0b19695333e3 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Mon, 14 Sep 2026 03:26:03 +0300 Subject: [PATCH 3/5] chore: pin final reviewed messaging SDK --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7a946fd9..d09e6016 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@napi-rs/keyring": "1.3.0", "@oclif/core": "^4.13.5", - "@polymorfa/sdk": "github:polymorfa/sdks#93223606d8bf1be9d462b432fc4992d2f71c2b36" + "@polymorfa/sdk": "github:polymorfa/sdks#60ed30c21b131c04ab027e131ca9c19d92b94ac8" }, "bin": { "polymorfa": "bin/run.js" @@ -972,8 +972,8 @@ }, "node_modules/@polymorfa/sdk": { "version": "0.1.0-dev.0", - "resolved": "git+ssh://git@github.com/polymorfa/sdks.git#93223606d8bf1be9d462b432fc4992d2f71c2b36", - "integrity": "sha512-yb7VRB7xxdvRL5ooBhJdNchgtCWzCl8mZLpHGQkRsPXuJNAX3g8H7T2u1LdArd44vPKDb8gbVZqNja6sO+Dhyw==", + "resolved": "git+ssh://git@github.com/polymorfa/sdks.git#60ed30c21b131c04ab027e131ca9c19d92b94ac8", + "integrity": "sha512-T5m9TtBDqeYaNil+tn+a1QtyYfsZ8V6OnCd971dvZluR0oFOxpLouVMDKDxoiYPf+gKSdueggyC3cAeuDKmwaQ==", "workspaces": [ "packages/calls", "packages/browser", diff --git a/package.json b/package.json index b5095105..4637e550 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "dependencies": { "@napi-rs/keyring": "1.3.0", "@oclif/core": "^4.13.5", - "@polymorfa/sdk": "github:polymorfa/sdks#93223606d8bf1be9d462b432fc4992d2f71c2b36" + "@polymorfa/sdk": "github:polymorfa/sdks#60ed30c21b131c04ab027e131ca9c19d92b94ac8" }, "devDependencies": { "@eslint/js": "^9.39.5", From 3d6f646209c8b6c5872c5919dbc1b6d1da733793 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Mon, 14 Sep 2026 03:39:09 +0300 Subject: [PATCH 4/5] chore: use reviewed signaling and media SDK contract --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index d09e6016..5981cc37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@napi-rs/keyring": "1.3.0", "@oclif/core": "^4.13.5", - "@polymorfa/sdk": "github:polymorfa/sdks#60ed30c21b131c04ab027e131ca9c19d92b94ac8" + "@polymorfa/sdk": "github:polymorfa/sdks#da65b22a8c4bf79aa378cf1848d2b7cc765ba326" }, "bin": { "polymorfa": "bin/run.js" @@ -972,8 +972,8 @@ }, "node_modules/@polymorfa/sdk": { "version": "0.1.0-dev.0", - "resolved": "git+ssh://git@github.com/polymorfa/sdks.git#60ed30c21b131c04ab027e131ca9c19d92b94ac8", - "integrity": "sha512-T5m9TtBDqeYaNil+tn+a1QtyYfsZ8V6OnCd971dvZluR0oFOxpLouVMDKDxoiYPf+gKSdueggyC3cAeuDKmwaQ==", + "resolved": "git+ssh://git@github.com/polymorfa/sdks.git#da65b22a8c4bf79aa378cf1848d2b7cc765ba326", + "integrity": "sha512-+J3yBEucdKXEdNz+idSRgvnapgjFsslgX80nhbOX4zd9tc+vm6GKZskLYw9c5D81gr6uoJzY/CaNvMOKaNzCIw==", "workspaces": [ "packages/calls", "packages/browser", diff --git a/package.json b/package.json index 4637e550..27fcc977 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "dependencies": { "@napi-rs/keyring": "1.3.0", "@oclif/core": "^4.13.5", - "@polymorfa/sdk": "github:polymorfa/sdks#60ed30c21b131c04ab027e131ca9c19d92b94ac8" + "@polymorfa/sdk": "github:polymorfa/sdks#da65b22a8c4bf79aa378cf1848d2b7cc765ba326" }, "devDependencies": { "@eslint/js": "^9.39.5", From 1445d5f260c27b57517f29814c1ce59ea1169f50 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Mon, 14 Sep 2026 03:44:23 +0300 Subject: [PATCH 5/5] fix: retain channel pagination positions in human output --- README.md | 16 +++++++++------- src/services/channels.ts | 1 + test/commands/channels.test.ts | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 94e21608..6cfe54d3 100644 --- a/README.md +++ b/README.md @@ -101,9 +101,9 @@ explicit session: ```shell polymorfa group list --session session_123 -polymorfa group view 123@g.us --session session_123 -polymorfa group participants list 123@g.us --session session_123 -polymorfa group invite-link 123@g.us --session session_123 +polymorfa group view 750000000001 --session session_123 +polymorfa group participants list 750000000001 --session session_123 +polymorfa group invite-link 750000000001 --session session_123 ``` Add `--json` for complete resource data and response metadata. Group and @@ -327,24 +327,24 @@ Channels are connected-session resources and require a server API key: ```shell polymorfa channel list --session session_123 -polymorfa channel view 120363000000000000@newsletter --session session_123 +polymorfa channel view 760000000001 --session session_123 polymorfa channel create \ --session session_123 \ --name 'Product updates' \ --description 'Release announcements' -polymorfa channel message list 120363000000000000@newsletter \ +polymorfa channel message list 760000000001 \ --session session_123 \ --limit 25 \ --before 951 -polymorfa channel message updates 120363000000000000@newsletter \ +polymorfa channel message updates 760000000001 \ --session session_123 \ --since 1787212800 polymorfa channel live-updates subscribe \ - 120363000000000000@newsletter \ + 760000000001 \ --session session_123 ``` @@ -355,6 +355,8 @@ does not start a listener, socket, or watch process, and the API exposes no unsubscribe operation. Message actions are `channel message mark-viewed` and `channel message react`. +Human output includes both the public message ID for actions and `position=N` +for the numeric `--before` and `--after` pagination arguments. An empty reaction removes the caller's reaction. Channel-level actions are `follow`, `unfollow`, `mute`, and `unmute`. Mutations accept `--idempotency-key`, and deletion additionally requires `--yes`. diff --git a/src/services/channels.ts b/src/services/channels.ts index 033aee0b..16d458b5 100644 --- a/src/services/channels.ts +++ b/src/services/channels.ts @@ -227,6 +227,7 @@ export function renderChannelMessages( .map((message) => [ message.id, + `position=${message.position}`, message.timestamp, message.type, `${message.views} views`, diff --git a/test/commands/channels.test.ts b/test/commands/channels.test.ts index 742587f8..3c1f5035 100644 --- a/test/commands/channels.test.ts +++ b/test/commands/channels.test.ts @@ -151,6 +151,22 @@ describe('channel commands', () => { ) }) + test.each(['list', 'updates'])( + 'keeps action ID and pagination position in human %s output', + async (command) => { + server = await startHttpServer({ + body: {data: [channelMessage()], success: true}, + }) + const result = await apiKeyEnvironment(() => + runCommand( + `channel message ${command} 760000000001 --session session_1`, + ), + ) + expect(result.error).toBeUndefined() + expect(result.stdout).toContain('760000000951\tposition=951') + }, + ) + test('maps update filters without inventing a returned cursor', async () => { server = await startHttpServer({ body: {data: [{...channelMessage(), text: undefined}], success: true},