Repository navigation
Add Hybrid Link commands and operation recovery #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
02b2204
bb16017
796cac9
f9ba7a1
9687b5b
72087d8
70fdc8d
e94b4b3
cbab603
b5713fc
c302cd6
33c09ff
3ddfac1
d0fbaea
b3fb09d
53dea71
e18cea8
d9049fd
536904e
651e16c
f3bd128
cae0941
5466f75
15d3770
333828b
f89bf21
e19a96a
c4a5ac8
55975a3
017dfac
3fbd744
9e5afc3
10419a3
7e41210
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,4 +2,5 @@ node_modules/ | |
| dist/ | ||
| coverage/ | ||
| *.tgz | ||
| .sdk-source/ | ||
| .DS_Store | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,3 +3,4 @@ dist/ | |
| node_modules/ | ||
| package-lock.json | ||
| *.tgz | ||
| .sdk-source/ | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -148,8 +148,11 @@ polymorfa quicklink cancel ql_123 \ | |||||||||||||
|
|
||||||||||||||
| Creation prints the hosted URL. Treat it as a credential and share it only with | ||||||||||||||
| the person holding the phone. The CLI never retrieves direct session QR payloads | ||||||||||||||
| or pairing codes. Cancel requires `--yes`, removes the pending session, and | ||||||||||||||
| fails for a link that has already connected. The API does not accept an | ||||||||||||||
| or pairing codes. Cancel requires `--yes` and invalidates an unconnected link. | ||||||||||||||
| For an initial real link, the API returns `202` after accepting cancellation; | ||||||||||||||
| Number and session cleanup can continue afterward. Supplementary and test | ||||||||||||||
| links return `200` when cancellation completes. Cancellation fails for a link | ||||||||||||||
| that has already connected. The API does not accept an | ||||||||||||||
| idempotency key for cancellation. The API exposes no QuickLink list, recovery, | ||||||||||||||
| or history operation, so the CLI does not synthesize them. | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -264,6 +267,10 @@ polymorfa message star 740000000001 \ | |||||||||||||
| --chat +15551234567 | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| JSON message output preserves `whatsapp_ids.linked_devices` and | ||||||||||||||
| `whatsapp_ids.official_api` when supplied by the API. Use the Polymorfa message | ||||||||||||||
| `id` for replies and actions; provider references remain strings. | ||||||||||||||
|
|
||||||||||||||
| Add `--remove` to `message star` to unstar. Text sends accept repeatable | ||||||||||||||
| `--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 | ||||||||||||||
|
|
@@ -1715,9 +1722,13 @@ export POLYMORFA_API_KEY='pmfa_...' | |||||||||||||
| polymorfa api /v1/projects --surface platform --json | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| Messaging accepts exactly one of `POLYMORFA_API_KEY` or | ||||||||||||||
| `POLYMORFA_CLIENT_TOKEN`. Platform accepts only `POLYMORFA_API_KEY`. Supplying | ||||||||||||||
| both Messaging credentials is an error rather than an implicit preference. | ||||||||||||||
| Messaging accepts exactly one of `POLYMORFA_API_KEY`, `POLYMORFA_PROJECT_TOKEN`, | ||||||||||||||
| or `POLYMORFA_CLIENT_TOKEN`. Project tokens remain confined to their project; | ||||||||||||||
| individual commands enforce their credential requirements. Raw Platform requests | ||||||||||||||
| accept only `POLYMORFA_API_KEY`. Supplying multiple Messaging credentials fails | ||||||||||||||
| before a request is sent. For a project-token profile, `--project` or the | ||||||||||||||
| nearest `polymorfa.json` must match the profile's project. Raw requests with an | ||||||||||||||
| API key or client token do not read the directory link. | ||||||||||||||
|
|
||||||||||||||
| Examples: | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -1890,6 +1901,57 @@ Read `GET /platform/sessions/{sessionId}/tier-quotes/{quoteId}` until the result | |||||||||||||
| applied or rejected. Store the full JSON request in a file and pass `--input` when | ||||||||||||||
| it contains numeric values or `null`; `--field` creates string values. | ||||||||||||||
|
|
||||||||||||||
| ### Hybrid Link controls | ||||||||||||||
|
|
||||||||||||||
| These additions are under development and are not available in the published | ||||||||||||||
| CLI. They are tested against the companion Hybrid Link SDK source using API | ||||||||||||||
| contract `d5245f76d5d6bc7b9af439571bd1f157185b1b87`. The registry SDK dependency | ||||||||||||||
| must be updated after that SDK artifact is published before releasing this CLI. | ||||||||||||||
| The companion SDK defaults native requests to API revision `2026-09-22`. An | ||||||||||||||
| explicit `--api-version` or saved-profile version remains explicit; retired | ||||||||||||||
| versions fail without an automatic upgrade or resend. | ||||||||||||||
|
|
||||||||||||||
| These commands require API-authorized private-preview access. An installed CLI | ||||||||||||||
| or a transport flag does not enable Hybrid Link. Use a team API key or project | ||||||||||||||
| token through `POLYMORFA_PROJECT_TOKEN` or a project-token profile. Project tokens | ||||||||||||||
| can control only their project and its Numbers; team routing policies require a | ||||||||||||||
| team API key. Browser client tokens cannot control Hybrid Link or send through it. | ||||||||||||||
|
|
||||||||||||||
| ```bash | ||||||||||||||
| polymorfa quicklink availability --project PROJECT_UUID --session NUMBER | ||||||||||||||
| polymorfa quicklink create --project PROJECT_UUID --connection-goal hybrid | ||||||||||||||
| polymorfa quicklink create --project PROJECT_UUID --purpose add_connection --session NUMBER --add-connection linked_devices | ||||||||||||||
| polymorfa message send text --session NUMBER --to +14155550123 --body Hello --transport official_api | ||||||||||||||
| polymorfa message operation-status OPERATION_UUID --session NUMBER | ||||||||||||||
| polymorfa routing-policy get --scope session --project PROJECT_UUID --session NUMBER | ||||||||||||||
| polymorfa routing-policy set --scope session --project PROJECT_UUID --session NUMBER --expected-revision REVISION --prefer linked_devices --allow linked_devices --allow official_api | ||||||||||||||
| polymorfa hybrid-link state --session NUMBER | ||||||||||||||
| polymorfa hybrid-link pause --session NUMBER --expected-revision REVISION | ||||||||||||||
| polymorfa hybrid-link resume --session NUMBER --expected-revision REVISION | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| For project and session routing scopes, `--project`, the nearest linked project, | ||||||||||||||
| and the selected profile determine the project in that order. A project-token | ||||||||||||||
| profile fails when its project differs from the linked directory. Team routing | ||||||||||||||
| scope uses an organization API key and ignores directory project links. `quicklink | ||||||||||||||
| availability` shows existing connection status and any QuickLink that can be | ||||||||||||||
| resumed. | ||||||||||||||
|
|
||||||||||||||
| All message send types, reactions, edits, and deletions accept `--transport auto`, | ||||||||||||||
| `linked_devices`, or `official_api`. Explicit choices never fall back. JSON output | ||||||||||||||
| preserves route metadata and the operation ID. If a send reports an unknown | ||||||||||||||
| outcome, inspect `message operation-status`; do not resend with a new key or change | ||||||||||||||
| transport. Status reads do not send. `pending` and `unknown` are unresolved outcomes. | ||||||||||||||
| `rejected` means the runtime proved it denied the send before a provider attempt. | ||||||||||||||
| Fix the access or routing cause before creating a new operation with a new | ||||||||||||||
| idempotency key. The rejected operation itself cannot be resumed. | ||||||||||||||
|
|
||||||||||||||
| Policy writes require the revision returned by `routing-policy get` (`default` | ||||||||||||||
| for an unsaved policy). Pause/resume requires the revision returned by | ||||||||||||||
| `hybrid-link state`. A stale write fails without replacing later changes. | ||||||||||||||
|
Comment on lines
+1949
to
+1951
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Fix the pause/resume revision description. Line 1838 says pause/resume requires "the UUID returned by 📝 Proposed fix-for an unsaved policy). Pause/resume requires the UUID returned by
-`hybrid-link state`. A stale write fails without replacing later changes.
+for an unsaved policy). Pause/resume requires the revision returned by
+`hybrid-link state`. A stale write fails without replacing later changes.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| `--prefer inherit` sends no local preference. `--deny-all` disallows both transports at the selected scope. Repeating `--allow` permits both | ||||||||||||||
| transports within the restrictions inherited from the team and project. | ||||||||||||||
|
|
||||||||||||||
| ### Functions | ||||||||||||||
|
|
||||||||||||||
| Functions are project-owned. Select `--project` or use the linked project/profile context. The commands accept an organization API key or a project token; browser client tokens are refused. Access also depends on the team's enabled Functions release and the configured execution region. | ||||||||||||||
|
|
||||||||||||||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -95,7 +95,7 @@ export abstract class BaseCommand extends Command { | |
| const session = await createDeveloperSession( | ||
| flags, | ||
| (secret) => this.registerSecret(secret), | ||
| await this.linkedProject(flags), | ||
| () => this.linkedProject(flags), | ||
| ) | ||
| this.errorContext = session.context | ||
| return session | ||
|
|
@@ -131,15 +131,35 @@ export abstract class BaseCommand extends Command { | |
|
|
||
| protected async messagingClient( | ||
| flags: GlobalFlags, | ||
| options: {readonly serverApiKeyOnly?: string} = {}, | ||
| options: { | ||
| readonly ignoreProjectLink?: boolean | ||
| readonly serverApiKeyOnly?: string | ||
| readonly serverCredentialOnly?: string | ||
| } = {}, | ||
| ): Promise<MessagingClient> { | ||
| const environment = await this.messagingEnvironment(flags) | ||
| const environment = await this.messagingEnvironment(flags, { | ||
| allowProjectToken: | ||
| options.serverCredentialOnly !== undefined || | ||
| options.serverApiKeyOnly !== undefined, | ||
| readProjectLink: | ||
| options.serverCredentialOnly !== undefined && | ||
| options.ignoreProjectLink !== true, | ||
|
Comment on lines
+144
to
+146
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When any of the new Useful? React with 👍 / 👎. |
||
| }) | ||
| if ( | ||
| options.serverApiKeyOnly !== undefined && | ||
| environment.credential.type !== 'apiKey' | ||
| ) { | ||
| throw new PolymorfaConfigurationError( | ||
| `${options.serverApiKeyOnly} commands require a server API key; Messaging client tokens do not have ${options.serverApiKeyOnly} access. Set POLYMORFA_API_KEY or select an API-key profile.`, | ||
| `${options.serverApiKeyOnly} commands require a server API key; project and Messaging client tokens do not have ${options.serverApiKeyOnly} access. Set POLYMORFA_API_KEY or select an API-key profile.`, | ||
| 'credential', | ||
| ) | ||
| } | ||
| if ( | ||
| options.serverCredentialOnly !== undefined && | ||
| environment.credential.type === 'clientToken' | ||
| ) { | ||
| throw new PolymorfaConfigurationError( | ||
| `${options.serverCredentialOnly} commands require a server API key or project token; Messaging client tokens do not have this access. Set POLYMORFA_API_KEY or POLYMORFA_PROJECT_TOKEN, or select a server-credential profile.`, | ||
| 'credential', | ||
| ) | ||
| } | ||
|
|
@@ -151,7 +171,9 @@ export abstract class BaseCommand extends Command { | |
| flags: GlobalFlags, | ||
| ): Promise<MessagingClient> { | ||
| return createMessagingClient( | ||
| await this.resolveEnvironment(flags, 'messaging', true), | ||
| await this.resolveEnvironment(flags, 'messaging', { | ||
| allowProjectToken: true, | ||
| }), | ||
| ) | ||
| } | ||
|
|
||
|
|
@@ -211,8 +233,12 @@ export abstract class BaseCommand extends Command { | |
| /** Resolve Messaging credentials once, for commands that also open sockets. */ | ||
| protected async messagingEnvironment( | ||
| flags: GlobalFlags, | ||
| options: { | ||
| readonly allowProjectToken?: boolean | ||
| readonly readProjectLink?: boolean | ||
| } = {}, | ||
| ): Promise<ResolvedEnvironment> { | ||
| return this.resolveEnvironment(flags, 'messaging') | ||
| return this.resolveEnvironment(flags, 'messaging', options) | ||
| } | ||
|
|
||
| protected async platformClient( | ||
|
|
@@ -251,12 +277,19 @@ export abstract class BaseCommand extends Command { | |
| private async resolveEnvironment( | ||
| flags: GlobalFlags, | ||
| surface: ResolvedEnvironment['surface'], | ||
| allowProjectToken = false, | ||
| options: { | ||
| readonly allowProjectToken?: boolean | ||
| readonly readProjectLink?: boolean | ||
| } = {}, | ||
| ): Promise<ResolvedEnvironment> { | ||
| const environment = await resolveCommandEnvironment({ | ||
| env: process.env, | ||
| surface, | ||
| ...(allowProjectToken ? {allowProjectToken: true} : {}), | ||
| ...(options.allowProjectToken === true ? {allowProjectToken: true} : {}), | ||
| ...(surface === 'messaging' && options.readProjectLink === true | ||
| ? {loadLinkedProject: () => this.linkedProject(flags)} | ||
| : {}), | ||
| ...(flags.project === undefined ? {} : {project: flags.project}), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a project-token profile bound to project A is used from a directory linked to project B without an explicit Useful? React with 👍 / 👎. |
||
| ...(flags.profile === undefined ? {} : {profile: flags.profile}), | ||
| ...(flags['api-version'] === undefined | ||
| ? {} | ||
|
|
@@ -297,6 +330,9 @@ export abstract class BaseCommand extends Command { | |
| ? {} | ||
| : {docUrl: presented.docUrl}), | ||
| retryable: presented.retryable, | ||
| ...(presented.operationId === undefined | ||
| ? {} | ||
| : {operationId: presented.operationId}), | ||
| ...(presented.retryAfterSeconds === undefined | ||
| ? {} | ||
| : {retryAfterSeconds: presented.retryAfterSeconds}), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -123,6 +123,13 @@ export default class Api extends BaseCommand { | |
| const environment = await resolveCommandEnvironment({ | ||
| env: process.env, | ||
| surface: flags.surface, | ||
| allowProjectToken: flags.surface === 'messaging', | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| ...(flags.surface === 'messaging' | ||
| ? { | ||
| project: flags.project, | ||
| loadLinkedProject: () => this.linkedProject(flags), | ||
| } | ||
| : {}), | ||
| ...(flags.profile === undefined ? {} : {profile: flags.profile}), | ||
| ...(flags['api-version'] === undefined | ||
| ? {} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import {Flags} from '@oclif/core' | ||
| import {BaseCommand} from '../../base-command.js' | ||
|
|
||
| export default class HybridLinkPause extends BaseCommand { | ||
| static override description = | ||
| 'Pause Hybrid routing at the exact current revision.' | ||
| static override flags = { | ||
| session: Flags.string({ | ||
| required: true, | ||
| description: 'Number session name.', | ||
| }), | ||
| 'expected-revision': Flags.string({ | ||
| required: true, | ||
| description: 'Revision from hybrid-link state.', | ||
| }), | ||
| } | ||
| async run(): Promise<void> { | ||
| const {flags} = await this.parse(HybridLinkPause) | ||
| const client = await this.messagingClient(flags, { | ||
| serverCredentialOnly: 'Hybrid Link', | ||
| }) | ||
| const response = await client.hybridLink.setPaused(flags.session, { | ||
| expectedRevision: flags['expected-revision'], | ||
| paused: true, | ||
| }) | ||
| this.writeValue( | ||
| response, | ||
| `Hybrid Link ${response.data.data.paused ? 'paused' : 'active'}: ${response.data.data.revision}`, | ||
| flags, | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import {Flags} from '@oclif/core' | ||
| import {BaseCommand} from '../../base-command.js' | ||
|
|
||
| export default class HybridLinkResume extends BaseCommand { | ||
| static override description = | ||
| 'Resume Hybrid routing at the exact current revision.' | ||
| static override flags = { | ||
| session: Flags.string({ | ||
| required: true, | ||
| description: 'Number session name.', | ||
| }), | ||
| 'expected-revision': Flags.string({ | ||
| required: true, | ||
| description: 'Revision from hybrid-link state.', | ||
| }), | ||
| } | ||
| async run(): Promise<void> { | ||
| const {flags} = await this.parse(HybridLinkResume) | ||
| const client = await this.messagingClient(flags, { | ||
| serverCredentialOnly: 'Hybrid Link', | ||
| }) | ||
| const response = await client.hybridLink.setPaused(flags.session, { | ||
| expectedRevision: flags['expected-revision'], | ||
| paused: false, | ||
| }) | ||
| this.writeValue( | ||
| response, | ||
| `Hybrid Link ${response.data.data.paused ? 'paused' : 'active'}: ${response.data.data.revision}`, | ||
| flags, | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a user follows this new documentation and runs
api --surface messagingwith onlyPOLYMORFA_PROJECT_TOKEN(or a project-token profile), the command still callsresolveCommandEnvironmentwithoutallowProjectToken, so the environment token is ignored and the profile is rejected before any request is made; with another environment credential present, the documented ambiguity check also ignores the project token. Enable project-token resolution for the raw Messaging surface or remove this advertised support.Useful? React with 👍 / 👎.