Skip to content

Integration: MAU-based account support and SDK dependency upgrades for next release - #467

Open
ttypic wants to merge 11 commits into
mainfrom
integration/mau
Open

ttypic wants to merge 11 commits into
mainfrom
integration/mau

Conversation

@ttypic

@ttypic ttypic commented Oct 13, 2026

Copy link
Copy Markdown

Integration branch collecting the changes required for the next CLI release to
work with MAU-based (monthly active users) Ably accounts. It also brings the
Pub/Sub, Chat and Spaces SDKs up to date, since the newer SDK versions are
needed for MAU-based account support.

ttypic and others added 4 commits October 5, 2026 19:54
Replace the `ably` dependency with `@ably/pubsub-server`, the split v3
package for servers, so every client the CLI builds declares itself a
server: server traffic is exempt from MAU counting. The CLI always
connects as a server, whatever it authenticates with. Types come from
the same package, which re-exports the core's type surface.

`createAblyRestClient()` / `createAblyRealtimeClient()` build clients
with its `createHttpClient` / `createRealtimeClient`.

`@ably/chat` and `@ably/spaces` move to their canaries built on the v3
core (chat 0.0.0-canary.20261001T1209.783b80e6, spaces
0.0.0-canary.20261001T1221.ca08adce), constructed with `createChatClient`
and `createSpacesClient` around the CLI's own realtime client, so rooms
and spaces traffic is server traffic too. Nothing depends on `ably` v2
any more.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
feat(mau): construct Pub/Sub clients with @ably/pubsub-server
Every client the CLI built used to mint a random `ably-cli-<uuid8>`, so a
30-command session acted as 30 different clients and left random names
in message history and presence.

The CLI now resolves its client ID once per process with the precedence
--client-id > ABLY_CLIENT_ID > `client.id` in the config > an install
default generated once and persisted in the config. Every client, every
interactive command and every minted token share it.

- Reject "" (which silently fell back to a random ID) and "*" with
  messages naming where the value came from; deprecate "none".
- Under token auth the identity is the token's and is never overridden;
  a JWT without an `x-ably-clientId` claim is rejected up front.
- Show the resolved client ID in the "Using:" banner.
- bench suffixes its default identity per process so concurrent bench
  processes stay under the per-client-ID connection cap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Resolve the acting identity once per command, bench suffix
  included, so the banner shows the ID the clients connect as
- Route explicit --client-id on the token commands through the
  shared resolver: "none" now warns as deprecated, and "*" is
  allowed so wildcard tokens can still be minted. With an explicit
  --client-id the issuing client acts as the token's identity, so
  the default identity is never resolved
- Under a wildcard ("*") JWT, act as --client-id, ABLY_CLIENT_ID or
  the default client ID, since the token lets its holder choose
- Keep the generated default ID in memory if the config can't be
  written, instead of failing every command
- Name the client.id config setting correctly in errors, say which
  source a deprecated "none" came from, and update the --client-id
  descriptions

Tests cover the banner matching the suffixed ID, wildcard tokens
when minting and connecting, source names in messages, and the
default ID's first-write and read-only-config cases.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ttypic
ttypic requested a review from umair-ably October 13, 2026 08:19
@vercel

vercel Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cli-web-cli Ready Ready Preview Oct 13, 2026 12:49pm UTC

Request Review

@claude-code-ably-assistant

Copy link
Copy Markdown

Walkthrough

This integration PR adds MAU (Monthly Active Users) account support by replacing the ably v2 dependency with @ably/pubsub-server — the server-specific split package whose traffic is exempt from MAU counting. It also upgrades @ably/chat and @ably/spaces to canary builds that are built on the same v3 core, ensuring all CLI-initiated connections are classified as server traffic.

Changes

Area Files Summary
Config package.json, pnpm-lock.yaml Replace ably with @ably/pubsub-server; bump @ably/chat and @ably/spaces to canary versions built on v3 core
Base Commands src/base-command.ts, src/chat-base-command.ts, src/spaces-base-command.ts Switch client construction to createHttpClient/createRealtimeClient from @ably/pubsub-server; use createChatClient/createSpacesClient wrappers
Commands 24 files under src/commands/ Update import type * as Ably from to use @ably/pubsub-server; type references updated from Ably.Rest/Ably.Realtime to Ably.PubSubHttpClient/Ably.PubSubRealtimeClient
Utils src/utils/history.ts, src/utils/message.ts, src/utils/output.ts Align type imports with new package
Tests test/helpers/, test/setup.ts, test/e2e/, test/unit/ Update mock helpers and e2e auth helpers for new client types; minor cleanup
Docs AGENTS.md, docs/Testing.md Update package name references to reflect new dependency
Skills .claude/skills/ably-new-command/SKILL.md, references/patterns.md Update AI coding conventions to reference @ably/pubsub-server types and construction patterns

Review Notes

  • Breaking for local dev: Anyone with a locally built copy will need to pnpm install to pick up the new @ably/pubsub-server package and the updated canary builds of Chat/Spaces.
  • Canary dependencies: @ably/chat and @ably/spaces are pinned to specific canary SHA builds (canary.20261001T1209.783b80e6 / canary.20261001T1221.ca08adce). These are pre-release — reviewers should confirm these canaries are stable enough for a release branch and that there's a plan to pin to stable versions before shipping.
  • Type surface: Ably.PubSubRealtimeClient / Ably.PubSubHttpClient replace Ably.Realtime / Ably.Rest throughout. Verify that all SDK method calls still compile correctly — the diff is +216/-209 lines across ~45 files, mostly mechanical renaming.
  • connections/test.ts has the largest non-trivial command change (+7/-6); worth a closer look to ensure the connection test logic is still correct after the client construction changes.
  • Test helpers (ably-event-emitter.ts +12/-3, e2e-test-helper.ts +6/-5): more substantial changes than other test files — confirm mocks still faithfully represent the new client API shape.

@claude-code-ably-assistant claude-code-ably-assistant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Integration/MAU — SDK migration to @ably/pubsub-server

This is a well-scoped SDK migration PR. The mechanical changes are correct and complete — no leftover ably package imports remain anywhere in src/ or test/. The overall approach is solid.

One item worth flagging

test/helpers/ably-event-emitter.ts — module-level client instantiation with undocumented internal access

The new code creates a real createRealtimeClient(...) call at module import time to reach the internal EventEmitter constructor:

const realtimeClient = createRealtimeClient({
  key: "appId.keyId:keySecret",
  autoConnect: false,
});
export const EventEmitter = (
  realtimeClient.constructor as unknown as { EventEmitter: new () => AblyEventEmitter }
).EventEmitter;
realtimeClient.close();

Two concerns:

  1. Fragility: .constructor.EventEmitter is an undocumented SDK internal. The old code accessed Ably.Realtime.EventEmitter (a static on the class — also undocumented, but at least reachable without instantiation). The new pattern's access path breaks silently at runtime if the SDK restructures its class hierarchy — TypeScript won't catch it because the cast to unknown as { EventEmitter: ... } bypasses type checking. A runtime undefined is not a constructor error in tests could be hard to trace.

  2. Module-level side effect: The client is created (and closed) when the module is first imported, not lazily inside a test. While autoConnect: false prevents WebSocket connections, the SDK still runs initialization code. This is a minor concern but worth being aware of.

Why it had to change: The old approach worked because ably exported the Realtime constructor as a named export, making Ably.Realtime.EventEmitter directly accessible. @ably/pubsub-server only exports factories (createRealtimeClient), not the class itself, so there's no static property to access without an instance. This is a genuine API surface limitation.

Suggested path: Ask the @ably/pubsub-server team whether EventEmitter can be exported directly, or expose a test helper API. Alternatively, a simple hand-rolled EventEmitter shim in test helpers that satisfies the AblyEventEmitter interface would remove the SDK dependency entirely.

This doesn't block the PR — it's test helper code, not production code, and the behavior should work correctly in practice. But it's worth tracking.


Everything else looks good

  • All new Ably.Rest(...) → createHttpClient(...) ✓
  • All new Ably.Realtime(...) → createRealtimeClient(...) ✓
  • All new ChatClient(...) → createChatClient(...) ✓
  • The dynamic-import hack in spaces-base-command.ts (20+ lines of moduleAsRecord.default.default || ... fallbacks) replaced with a clean synchronous createSpacesClient() call — net improvement
  • The x-ably-clientType: "server" JWT claim added to the E2E revocation test is a correct fix; @ably/pubsub-server always connects as a server and token auth enforces this
  • spaces-base-command.createSpacesClient correctly changed from async to sync to match the new synchronous factory
  • Type renames (Ably.Rest → Ably.PubSubHttpClient, Ably.Realtime → Ably.PubSubRealtimeClient) are consistent throughout

feat(mau): resolve one stable client identity per process
Push and token commands declare their own --client-id to name a target
or filter, but the base command read `flags["client-id"]` regardless of
which definition supplied it. `ably push devices remove-where
--client-id user123` therefore also connected as user123, billing an
end user who never connected and misattributing an admin write.

The base command now honours --client-id as the CLI's own identity only
when the command declares the shared `clientIdFlag` definition. Flag
names are unchanged; the eleven local target flags get descriptions
that say what they target. A guard test loads every command and fails
if any local --client-id reaches the client options.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

� Conflicts:
�	src/base-command.ts
�	src/commands/auth/issue-ably-token.ts
�	src/commands/auth/issue-jwt-token.ts
fix(mau): stop target --client-id flags from becoming the CLI's identity
ttypic and others added 2 commits October 13, 2026 13:29
- Token output always states the identity: `Client ID: anonymous` in
  human output and `clientId: null` in JSON, rather than omitting it.
- `auth issue-jwt-token --client-type server` adds the signed
  `x-ably-clientType=server` claim, which is the only way a
  token-authenticated client can be classified as a server.
- `auth revoke-token` goes through the SDK's `auth.revokeTokens` instead
  of a hand-rolled HTTPS call to a hardcoded rest.ably.io, so it honours
  the configured endpoint and reports per-target failures.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
feat(mau): make token issuance and revocation identity-safe
…them

- Add a hint for 40012 (invalid client ID), and make the client ID hints
  (40012, 40161, 91000) context-aware: under token auth they point at
  re-issuing the token, and they only suggest --client-id on commands
  that have it, falling back to ABLY_CLIENT_ID.
- Spaces connection and cursor-attach failures keep the Ably error code
  and status instead of wrapping them in a plain Error.
- Long-running commands fail with a non-zero exit when their connection
  dies mid-run: on `failed`, or when it drops five times in a row without
  staying connected for 10s, which is how a server-side rejection such
  as an eviction can present. Previously a subscribe that lost its
  connection logged a warning and exited 0.
- The --duration exit path force-exits before finally() runs, so it now
  emits the standard `completed` JSON status itself instead of an ad-hoc
  `complete` line, and prints its human message to stderr.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

� Conflicts:
�	src/base-command.ts
fix(mau): surface identity and connection failures instead of hiding them

This branch was successfully deployed

1 active deployment
Preview — 9e3192da Deployed Oct 13, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants