Skip to content
Open
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
6 changes: 4 additions & 2 deletions .claude/skills/ably-new-command/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ static flags = {
- `await this.requireAppId(flags)` — resolves and validates the app ID, returns `Promise<string>` (non-nullable). Calls `this.fail()` internally if no app found — no manual null check needed.
- `await this.runControlCommand(flags, api => api.method(appId))` — creates the Control API client, executes the call, and handles errors in one step. Returns `Promise<T>` (non-nullable). Useful for single API calls; for multi-step flows, use `this.createControlApi(flags)` directly.

**When to include `clientIdFlag`:** Add `...clientIdFlag` to commands where client identity affects the operation: subscribe, publish, enter, set, acquire, update, delete, append, annotate. The reason is that users may want to test auth scenarios — e.g., "can client B update client A's message?" — so they need the ability to set their client ID. Do NOT add to read-only queries (get, get-all, history, occupancy get) — Ably capabilities are operation-based, not clientId-based, so client identity is irrelevant for pure reads.
**When to include `clientIdFlag`:** Add `...clientIdFlag` to commands where client identity affects the operation: subscribe, publish, enter, set, acquire, update, delete, append, annotate. The reason is that users may want to test auth scenarios — e.g., "can client B update client A's message?" — so they need the ability to set their client ID. Do NOT add to read-only queries (get, get-all, history, occupancy get). Reads still carry identity — every command acts as the session's client ID (`ABLY_CLIENT_ID` > config > generated default) and Ably counts it under MAU billing — but a read has no reason to act as someone else, so the session identity is enough.

**Target client IDs are not identity:** the base command treats `--client-id` as the client ID the CLI acts as only when the command declares it via the shared `clientIdFlag` object. When a command's `--client-id` names a *target* or *filter* (a push recipient, a token's subject), declare a command-local `Flags.string()` instead — never spread `clientIdFlag` for that — so the value can't become the CLI's own identity. `test/unit/base/client-id-target-guard.test.ts` enforces this for every command.

For history commands, also use `timeRangeFlags`:
```typescript
Expand Down Expand Up @@ -472,7 +474,7 @@ See the "Keeping Skills Up to Date" section in `CLAUDE.md` for the full list of
- [ ] JSON output uses `logJsonResult()` (one-shot) or `logJsonEvent()` (streaming), not direct `formatJsonRecord()`
- [ ] JSON data nested under a domain key (singular for events/single items, plural for collections) — not spread at top level (see `patterns.md` "JSON Data Nesting Convention")
- [ ] `room.attach()` / `space.enter()` only called when the SDK method requires a realtime connection (subscribe, send, presence) — NOT for REST mutations (update, delete, annotate, history)
- [ ] Subscribe/enter commands use `this.waitAndTrackCleanup(flags, component, flags.duration)` (not `waitUntilInterruptedOrTimeout`)
- [ ] Subscribe/enter commands use `this.waitAndTrackCleanup(flags, component, flags.duration)` (not `waitUntilInterruptedOrTimeout`) and call `this.setupConnectionStateLogging(client, flags)` — together they fail the command (non-zero exit) when the connection fails or keeps dropping, instead of exiting 0
- [ ] Error handling uses `this.fail()` exclusively, not `this.error()` or `this.log(chalk.red(...))`
- [ ] Component strings are camelCase: single-word lowercase (`"room"`, `"auth"`), multi-word camelCase (`"channelPublish"`, `"roomPresenceSubscribe"`)
- [ ] At least one `--json` example in `static examples`
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/ably-new-command/references/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ async run(): Promise<void> {

## Get Pattern

Get commands perform one-shot read-only queries for current state. They don't need `clientIdFlag` (Ably capabilities are operation-based, not clientId-based — client identity is irrelevant for reads), `durationFlag`, or `rewindFlag`.
Get commands perform one-shot read-only queries for current state. They don't need `clientIdFlag` (they act as the session's client ID, which is enough for a read), `durationFlag`, or `rewindFlag`.

```typescript
static override flags = {
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ This is the Ably CLI npm package (`@ably/cli`), built with the [oclif framework]
│ ├── integration/ # Multi-component, mocked external services
│ ├── e2e/ # Full scenarios against real Ably
│ └── helpers/ # runCommand(), MockConfigManager, etc.
├── docs/ # Project docs (Testing.md, Project-Structure.md, etc.)
├── docs/ # Project docs (Testing.md, Project-Structure.md, Client-Identity.md, etc.)
└── package.json # Scripts defined here
```

Expand Down Expand Up @@ -100,7 +100,7 @@ Flags are NOT global. Each command explicitly declares only the flags it needs v
- **`coreGlobalFlags`** — `--verbose`, `--json`, `--pretty-json`, `--web-cli-help` (hidden) (on every command via `AblyBaseCommand.globalFlags`)
- **`productApiFlags`** — core + hidden product API flags (`port`, `tlsPort`, `tls`). Use for commands that talk to the Ably product API.
- **`controlApiFlags`** — core + hidden control API flags (`control-host`, `dashboard-host`). Use for commands that talk to the Control API.
- **`clientIdFlag`** — `--client-id`. Add to commands where client identity affects the operation: subscribe, publish, enter, set, acquire, update, delete, append. Do NOT add to read-only queries (get, get-all, occupancy get) — Ably capabilities are operation-based, not clientId-based, so client identity is irrelevant for pure reads. Do NOT add globally.
- **`clientIdFlag`** — `--client-id`. Add to commands where acting as a particular client is part of the operation: subscribe, publish, enter, set, acquire, update, delete, append. Do NOT add to read-only queries (get, get-all, occupancy get): every command, reads included, already acts as the session's client ID (`ABLY_CLIENT_ID` > config > generated default, see `docs/Client-Identity.md`) and Ably counts it under MAU billing, but a read has no reason to act as someone else. Do NOT add globally. A `--client-id` that names a *target* (push recipient, token subject) must be a command-local flag, never `clientIdFlag` — the base command treats only `clientIdFlag` as identity.
- **`durationFlag`** — `--duration` / `-D`. Use for long-running subscribe/stream commands that auto-exit after N seconds.
- **`rewindFlag`** — `--rewind`. Use for subscribe commands that support message replay (default: 0).
- **`timeRangeFlags`** — `--start`, `--end`. Use for history and stats commands. Parse with `parseTimestamp()` from `src/utils/time.ts`. Accepts ISO 8601, Unix ms, or relative (e.g., `"1h"`, `"30m"`, `"2d"`).
Expand Down Expand Up @@ -282,7 +282,7 @@ this.error() ← oclif exit (ONLY inside fail, nowhere else)
- **Never use `this.error()` directly** — it is an internal implementation detail of `this.fail()`.
- **`requireAppId`** returns `Promise<string>` (not nullable) — calls `this.fail()` internally if no app found.
- **`runControlCommand<T>`** returns `Promise<T>` (not nullable) — calls `this.fail()` internally on error.
- **Error hints**: `fail()` appends a CLI-specific hint from `src/utils/errors.ts` if one exists for the Ably error code. Hints must only contain actionable CLI advice (e.g., "run `ably login`"), not restate the upstream error message (which is already shown). When adding new error codes, **fetch** https://ably.com/docs/platform/errors/codes using WebFetch to get the official description — do NOT rely on memory or assumptions about what an error code means.
- **Error hints**: `fail()` appends a CLI-specific hint from `src/utils/errors.ts` if one exists for the Ably error code. A hint may be a function of `HintContext` (token auth or not, whether the command declares `--client-id`) so it never suggests a flag the command lacks. Hints must only contain actionable CLI advice (e.g., "run `ably login`"), not restate the upstream error message (which is already shown). When adding new error codes, **fetch** https://ably.com/docs/platform/errors/codes using WebFetch to get the official description — do NOT rely on memory or assumptions about what an error code means.
- **`extractErrorInfo(error)`** (`src/utils/errors.ts`) — extracts `{ message, code?, statusCode?, href? }` from an unknown error value. Use in commands that report per-item errors inline (batch publish results, connection test summaries) rather than exiting via `this.fail()`.

### Additional output patterns (direct chalk, not helpers)
Expand Down
68 changes: 68 additions & 0 deletions docs/Client-Identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Client Identity and MAU Classification

Ably bills monthly active users (MAU) per distinct client ID on **device** traffic; **server** traffic is exempt from MAU counting, from the per-client-ID connection cap, and from the requirement to carry a client ID. This page describes how the CLI declares its own traffic and which client ID it acts as. Design rationale: DXRFC-029 (CLI Classification and Identity for MAU Pricing).

## Always a server

The CLI always connects as a server. Every Pub/Sub client is built with `@ably/pubsub-server`, whatever the CLI authenticates with, and Chat and Spaces wrap that same client, so rooms and spaces traffic is server traffic too.

With an API key (`ably login`, `ABLY_API_KEY`) that is all Ably needs. Under token auth (`ABLY_TOKEN`) Ably grants the server side only through a signed `x-ably-clientType=server` claim on the token, and rejects a client that declares itself a server without it. Native Ably tokens cannot carry the claim, so use a JWT issued with:

```bash
ably auth issue-jwt-token --client-type server --client-id backend-worker
```

## Which client ID the CLI acts as

Under API key auth, the client ID is resolved once per process, from the first of:

1. `--client-id <id>`, on commands that offer it
2. `ABLY_CLIENT_ID`
3. `client.id` in the config file (`~/.ably/config`)
4. A default generated once per install (`ably-cli-<8 hex chars>`) and saved as `client.defaultId` in the config

Every client the command builds, every command in an interactive session, and every token minted by `ably auth issue-ably-token` / `issue-jwt-token` without `--client-id` uses that one ID. Commands without a `--client-id` flag still act as it. `ably bench` commands append a per-process suffix to the default, so that many concurrent bench processes stay under the per-client-ID connection cap.
Comment on lines +17 to +24

To give a machine or CI job a fixed identity, set it once:

```bash
export ABLY_CLIENT_ID="deploy-bot"
```

or in the config file:

```toml
[client]
id = "deploy-bot"
```

Values the CLI refuses:

- `""` — an empty client ID. It used to fall back silently to a random ID.
- `"*"` — the wildcard. The CLI acts as, and issues tokens to, one concrete client ID.
- `"none"` — deprecated. It still acts with no client ID, with a warning, but apps that require identified clients reject that traffic.
Comment on lines +39 to +43

Under token auth the client ID is the token's and is never overridden; `--client-id` is ignored with a warning. A JWT without an `x-ably-clientId` claim is rejected before connecting.

### Target client IDs are not identity

On push and token commands, `--client-id` names a *target* rather than the CLI's identity: `ably push devices list --client-id user123` lists that user's devices, and `ably auth issue-jwt-token --client-id alice` issues a token to alice. These never change who the CLI acts as.

## Simulating multiple users

An explicit `--client-id` always wins, so two terminals can act as two different clients:

```bash
# Terminal 1
ably channels presence enter my-channel --client-id alice

# Terminal 2
ably channels presence enter my-channel --client-id bob
```

Both connect as servers, so they are not counted or capped as devices. Ably counts client IDs wherever they appear, including in message payloads, so each simulated name may still register as an MAU. The CLI cannot reproduce device behaviour (MAU counting, the per-client-ID connection cap, identified-client enforcement); use an Ably SDK on the device side for that.

## When something goes wrong

- Rejections about the client ID (`40012`, `40161`, `91000`) carry a hint for how you authenticated: set `--client-id` / `ABLY_CLIENT_ID` under API key auth, or re-issue the token with a client ID under token auth.
- A long-running command whose connection fails, or keeps dropping without staying connected, exits non-zero instead of reporting success.
2 changes: 2 additions & 0 deletions docs/Environment-Variables/General-Usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ These environment variables are most commonly used during development as well as
| `ABLY_ENDPOINT` | Host Override | Override Realtime/REST API endpoint (host only) | SDK default |

> For development, testing, debugging, and internal variables, see [Development Stage Usage](Development-Usage.md).
>
> For how `ABLY_CLIENT_ID`, `--client-id` and tokens decide which client ID the CLI acts as, and why the CLI always connects as a server for MAU billing, see [Client Identity](../Client-Identity.md).
3 changes: 2 additions & 1 deletion docs/Project-Structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ This document outlines the directory structure of the Ably CLI project.
│ └── utils/
│ ├── channel-rule-display.ts # Channel rule human-readable display
│ ├── chat-constants.ts # Shared Chat SDK constants (REACTION_TYPE_MAP)
│ ├── connection-health.ts # Detects dead connections (failed, reconnect loops)
│ ├── server-url.ts # Local server URL parsing (parseServerUrl, formatServerUrl)
│ ├── errors.ts # Error utilities (errorMessage)
│ ├── errors.ts # Error utilities (errorMessage, errorWithReason, hints)
│ ├── interrupt-feedback.ts # Ctrl+C feedback messages
│ ├── json-formatter.ts # JSON output formatting (formatJson, formatMessageData)
│ ├── key-display.ts # Key capability formatting
Expand Down
Loading