Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 165 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ access until `@polymorfa/cli` is released publicly.
## Requirements

- Node.js 20 or newer
- A Polymorfa server API key for Platform requests
- A Polymorfa organization API key or exact-project token for developer requests
- A server API key or short-lived client token for Messaging requests

## Development installation
Expand Down Expand Up @@ -39,7 +39,7 @@ polymorfa/0.1.0-dev.0 sdk/0.1.0-dev.0
## Authentication profiles

Until the backend exposes a revocable CLI device-authorization flow, login
accepts a server API key only through standard input:
accepts an organization API key or exact-project token through standard input:

```shell
printf '%s' "$POLYMORFA_API_KEY" | \
Expand All @@ -48,9 +48,12 @@ printf '%s' "$POLYMORFA_API_KEY" | \
polymorfa auth list
polymorfa auth status --profile work
polymorfa auth use work

printf '%s' "$POLYMORFA_PROJECT_TOKEN" | \
polymorfa auth login --with-project-token --project project_123 --profile project-work
```

Profile metadata is written to the platform configuration directory. API keys
Profile metadata records its explicit project or organization owner. Credentials
are stored separately in macOS Keychain, Windows Credential Manager, or Linux
Secret Service. A keyring failure stops the command; the CLI never silently
falls back to plaintext. `auth status` reports local secure-store availability
Expand All @@ -59,6 +62,41 @@ in this development slice and does not yet claim remote identity validation.
Environment credentials take precedence over the active profile for automation.
An explicit `--profile <name>` takes precedence over environment credentials.

## QuickLinks

A QuickLink is a persistent hosted link that connects one WhatsApp number to a
new session in a project. Create one from a JSON request with an organization
API key that has `quicklink:manage`:

```shell
polymorfa quicklink create \
--project 11111111-1111-4111-8111-111111111111 \
--input quicklink.json
```

The request accepts `customerId`, `externalId`, and `configuration`. See
[Guided session creation and configuration](#guided-session-creation-and-configuration)
for the configuration fields.

Inspect or cancel a link with an organization API key or an exact-project
token that has `quicklink:manage`:

```shell
polymorfa quicklink view ql_123 \
--project 11111111-1111-4111-8111-111111111111

polymorfa quicklink cancel ql_123 \
--project 11111111-1111-4111-8111-111111111111 \
--yes
```

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
idempotency key for cancellation. The API exposes no QuickLink list, recovery,
or history operation, so the CLI does not synthesize them.

## Customers

Manage Customers through the Platform API with an organization server key and
Expand Down Expand Up @@ -505,8 +543,8 @@ support for project credentials, client tokens, or Cloud API sessions.
## Platform access and security

Organization server API keys can inspect the credential, membership, audit,
ban, incident, durable-operation, and project-token metadata made available by
the Platform API:
ban, incident, saved QuickLink settings, and project-token metadata made
available by the Platform API:

```shell
polymorfa key list
Expand Down Expand Up @@ -546,8 +584,8 @@ returns `null` in JSON when no settings row has been saved. Update
accepts one bounded JSON object from `--input`; the target project is selected
separately with `--project` and cannot be hidden in that input. Omitting the
project targets organization defaults. Widget logo fields are existing storage
identifiers, not file, URL, base64, upload, or download operations. Both
commands require the `quicklink:manage` scope.
identifiers, not file, URL, base64, upload, or download operations. Widget
access requires the `quicklink:manage` scope.

Settings accept `successCallbackUrl` and `failureCallbackUrl`: project-only
HTTPS destinations that the API copies into each link when it is issued. Set
Expand Down Expand Up @@ -609,6 +647,122 @@ The source exposes no campaign update, delete, validate, archive, duplicate,
recipient, event, history, replay, watch, or operation-cancel route. The CLI
does not substitute similarly named Platform campaign methods for these gaps.

## Developer events and webhooks

Developer resources always have an explicit owner. Project-owned commands need
`--owner project --project <id>`. Organization-owned commands use
`--owner organization` and reject a project override. A stored profile can
provide the owner, but the CLI never guesses it from a credential or retries a
request against a different owner.

```shell
polymorfa event list \
--owner project \
--project project_123 \
--type message.received

polymorfa event view event_123 \
--owner project \
--project project_123 \
--include-payload

polymorfa webhook create \
--owner project \
--project project_123 \
--input webhook.json \
--secret-out ./webhook.secret

polymorfa webhook test webhook_123 \
--owner project \
--project project_123 \
--event polymorfa.test

polymorfa webhook delivery list \
--owner project \
--project project_123 \
--status failed

polymorfa webhook delivery attempt list delivery_123 \
--owner project \
--project project_123
```

Webhook creation and secret rotation return a signing secret once. JSON and
noninteractive runs require `--secret-out`; the CLI preflights a new path and
writes it atomically with mode `0600`. Interactive runs can reveal the secret
once on the terminal. The secret never appears in JSON, MCP output, debug
messages, profiles, or command arguments. A server success followed by a local
write failure exits with `partial_success` and retains the safe mutation
receipt. It never retries rotation on its own.

List commands return opaque cursors. `--paginate` requires `--max-items`, which
is capped at 10,000. The CLI shrinks the last request to the remaining item
budget instead of fetching records it will discard.

Replay, delivery retry, and webhook test return an idempotency receipt and an
operation ID in the JSON `operation.id` field. Operation inspection is
console-only, so the CLI has no command to poll or cancel that operation. Use
`webhook delivery view` and `webhook delivery attempt list` to follow the
resulting delivery.

`event view` prints the retained payload in human output only with
`--include-payload`. `webhook delivery attempt view` prints the redacted
response excerpt that the API records for a failed HTTP response. Human output
escapes control characters in remote text.

## Local event listening and forwarding

`polymorfa listen` opens the project listener stream and can forward the exact
event body to local webhook handlers:

```shell
polymorfa listen \
--project project_123 \
--event 'message.*' \
--forward-to http://127.0.0.1:3000/webhooks \
--forward-secret-out ./local-forwarding.secret \
--checkpoint-file ./.polymorfa-listener-checkpoint
```

The CLI signs forwarded bodies with a separate local secret. Remote forwarding
is disabled unless `--allow-remote-forward` is set. Redirects are disabled by
default; explicit redirect following revalidates each hop. The listener writes
checkpoints only after stdout and every forwarding destination have accepted an
event. A cursor gap stops with a nonzero exit unless `--on-gap continue` was
selected.

## MCP listener controls

The package includes a standards-based stdio MCP server for Codex and Claude:

```shell
polymorfa mcp install codex
polymorfa mcp install claude
polymorfa mcp status
polymorfa mcp tools
polymorfa mcp doctor
```

The MCP tools `polymorfa_listen_start`, `polymorfa_listen_status`, and
`polymorfa_listen_stop` control listeners inside that stdio server process.
They do not create detached daemons. MCP responses contain listener IDs,
status, and safe counters, never primary credentials, listener credentials,
forwarding secrets, or unrestricted payloads.

## Agent skills

The npm package includes two portable skills under `skills/`:

- `polymorfa` establishes profile, owner, project, JSON, and mutation-safety
conventions for CLI and MCP use.
- `polymorfa-webhooks` covers event inspection, webhook testing, replay and
retry, local listening and forwarding, and one-time secret handling.

Copy or link the relevant skill directory into the skills folder used by your
agent host. The skill treats the installed command help as authoritative, so it
stays useful as the command surface grows without teaching agents to invent
routes or use raw requests for missing typed developer operations.

## Raw API requests

`polymorfa api` is the complete-access escape hatch inspired by `gh api`. It
Expand Down Expand Up @@ -661,6 +815,10 @@ SDK request IDs are preserved and active credentials are redacted.
`POLYMORFA_BASE_URL` exists only for development and controlled testing. It
overrides a profile endpoint and must not be placed in project files.

The raw command does not bypass owner, principal, or scope boundaries. Events,
webhooks, deliveries, and attempts use their typed commands; the CLI does not use `api` to synthesize a missing developer
workflow.

## Verification

```shell
Expand Down
25 changes: 9 additions & 16 deletions docs/command-research/campaigns.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,9 @@ campaign
resume <campaign> --project-slug <slug>
stop <campaign> --project-slug <slug> --yes
requeue <campaign> --project-slug <slug> [--include-skipped-error]

operation
view <operation> --surface messaging
```

The CLI uses only `MessagingClient.campaigns` and
`MessagingClient.operations.retrieve`. Every campaign command requires an
The CLI uses only `MessagingClient.campaigns`. Every campaign command requires an
organization Messaging API key. Reads require `campaigns:read`; creation and
lifecycle mutations require `campaigns:manage`. The CLI rejects browser client
tokens before transport. It does not broaden the SDK to project tokens even
Expand All @@ -64,12 +60,11 @@ distinct commands because the server enforces different source states. Stop is
a terminal transition and requires `--yes`.

Launch, pause, resume, and stop append durable commands and return an operation
ID plus the campaign's present state. Human output says “Accepted”, does not say
the transition completed, and gives the exact follow-up command:
`polymorfa operation view <operation> --surface messaging`. The existing
Platform operation lookup remains the default when `--surface` is omitted.
Polymorfa exposes a singular operation read, not a wait or watch API, so the CLI
does not reproduce `gh run watch` with hidden polling semantics.
ID plus the campaign's present state. Human output says “Accepted” and does not
say the transition completed. The returned IDs belong to the Messaging campaign
API. Operation inspection is console-only, so the CLI cannot poll them. The
unpublished `--surface messaging` selector was removed rather than retained as
a compatibility alias.

Requeue is different: it is a direct transaction and returns only the number
of recipients moved. Failed recipients are always considered;
Expand All @@ -95,11 +90,9 @@ or operation cancel route. It also exposes no response cursor. The CLI does not
substitute similarly named Platform campaign operations or use `raw.request` to
invent those workflows.

Stripe `listen` and Wrangler `tail` are valuable models for the later
Polymorfa event-development workflow, but both are backed by a real connected
stream. A campaign `watch`, general `listen`, or webhook forwarder remains
reserved until Polymorfa exposes the corresponding listener/session or event
history API.
Stripe `listen` and Wrangler `tail` informed the separate Polymorfa developer
event listener. It is not a campaign watch command and does not change the
campaign API gaps above.

At this exact baseline the SDK ledger records 331 total / 207 covered / 58
missing / 66 excluded / 0 partial / 0 changed operations.
118 changes: 118 additions & 0 deletions docs/command-research/developer-events-webhooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Developer events, webhooks, and listener research

- **Status:** Implementation complete against the accepted contract and exact
SDK pin; deployed Staging acceptance remains a separate gate
- **Reviewed:** 2026-09-16
- **Monorepo baseline:** `1600403db9dcf7996dbbb882375b1165859829d3`
- **SDK baseline:** `08452992a32a392de5fc3d0fa3954ba6addd7f32`
- **Listener protocol SHA-256:**
`0f324f0ce4aa6b39e9af9f4aed51fcce42aa818abae00f9e3e03eb551f35d1ee`

## Sources reviewed

- [GitHub CLI API](https://cli.github.com/manual/gh_api) for a complete-access
escape hatch that remains separate from curated workflows.
- [GitHub CLI run watch](https://cli.github.com/manual/gh_run_watch) for terminal
polling, interval handling, and exit behavior.
- [Stripe CLI listen](https://docs.stripe.com/stripe-cli/use-cli) and the
open-source `stripe-cli` proxy implementation for foreground event filtering,
local signing, and forwarding. Live transport belongs to the CLI rather than
the public language SDK.
- [Wrangler tail](https://developers.cloudflare.com/workers/wrangler/commands/#tail)
for explicit live-stream filters and reconnect-oriented operator feedback.
- The accepted Polymorfa developer API RFC, authoritative OpenAPI documents,
listener protocol, backend handlers, and public TypeScript SDK contract.

## Command tree

```text
event list|view|replay
webhook list|view|create|update|delete|test
webhook secret rotate
webhook delivery list|view|retry
webhook delivery attempt list|view
listen
mcp serve|install|uninstall|status|tools|doctor
```

Developer resources have an explicit `project` or `organization` owner. A
stored profile may fix that choice; otherwise `--owner` is required before any
transport. Project ownership also requires exact project context. The CLI never
probes one owner after another and never treats a credential prefix, resource
ID, or failed request as ownership evidence.

All REST commands use the unified SDK `Client` with its typed `events`,
`webhooks`, and `webhookDeliveries` resources. API contract
`1600403db9dcf7996dbbb882375b1165859829d3` serves developer operations only
under `/console/*` with a dashboard session and marks them unavailable to
machine credentials, and SDK `08452992a32a392de5fc3d0fa3954ba6addd7f32` has no
`operations` resource. The CLI therefore ships no `operation` commands; mutation
envelopes report only `operation.id`. `listen` is
different by design: the CLI requests a one-use, short-lived listener
authorization from the configured trusted API origin and opens the Hono SSE
stream itself. No public SDK listener or raw-request workaround is introduced.

## Output and automation

Reads and lists emit schema-versioned envelopes with resource data, owner
context, request metadata, and final-page metadata. Mutations add resource,
operation, and idempotency receipts; inapplicable values are explicit `null`.
Errors are stderr-only, typed, and retain their symbolic condition.

API contract 1600403 adds a nullable, redacted `response` excerpt to delivery
attempts. The pinned SDK passes it through untyped; JSON output keeps it and
human attempt output prints it with control characters escaped.

List collection requires both `--paginate` and `--max-items`; the maximum is
10,000. Each page request shrinks to the remaining budget. Cursors are opaque,
owner- and filter-bound, and cannot be combined with new filters.

## Safety and secret delivery

Delete, rotation, replay, and delivery retry require a
TTY confirmation or `--yes`. Validation and confirmation happen before the
idempotency key is generated or the transport begins. Every mutation accepts a
caller key up to 255 UTF-8 bytes and preserves its receipt.

Webhook create and secret rotation may return one secret once. Interactive TTY
output may reveal it once. JSON, noninteractive, and MCP workflows require an
exclusive atomic mode-0600 file; secret material is omitted from output and
diagnostics. API success followed by a local file failure is `partial_success`
and rotation is not retried automatically.

## Listener and forwarding

The listener validates filter grammar, project and cursor identifiers,
authorization binding, trusted origins, negotiated limits, every SSE field, and
every typed frame before acknowledging data. It supports UTF-8 and CRLF across
arbitrary chunks, rejects duplicate `event`, `id`, or `data` fields, and
enforces negotiated whole-record and partial-buffer limits. Invalid frames fail
closed and remain unacknowledged.

Events are processed sequentially, which is a stricter bound than negotiated
`maxInFlight`. Acknowledgement occurs only after all required sinks succeed:
stdout for JSONL-only, every destination for forwarding-only, or both stdout
and every destination for combined mode. Checkpoints store only the last
acknowledged cursor.

Forwarding uses ordinary unauthenticated fetch rather than SDK transport.
Loopback HTTP is accepted for development. Remote forwarding requires explicit
opt-in and HTTPS. Redirects are off by default; explicit redirect mode
revalidates the scheme, DNS result, and each address on every hop. Durable and
listener credentials are never forwarded. Local bodies receive an ephemeral
signature suitable for the SDK's exact-byte webhook verifier.

## Implementation-time follow-up

Before each later command extension, recheck the current SDK method and backend
contract, then compare the corresponding maintained OSS command. Record new
filters, pagination, idempotency, confirmation, redaction, and terminal-state
semantics here before adding flags. Do not bend the command tree around an
accidental backend gap; bring a missing product contract to cross-stack design
review before proposing a monorepo change.

The remaining acceptance gate is a deployed Staging E2E at exact Monorepo, SDK,
and CLI commits. It must cover ownership, webhook create/test, delivery and
attempt inspection, wait/watch, replay, forwarding, reconnect/resume, gap
handling, secret non-disclosure, and clean SIGINT. Local and hosted CI do not
substitute for that evidence.
Loading