Skip to content

[DX-919] Extends push support#169

Open
umair-ably wants to merge 8 commits intomainfrom
DX-919-push
Open

[DX-919] Extends push support#169
umair-ably wants to merge 8 commits intomainfrom
DX-919-push

Conversation

@umair-ably
Copy link
Contributor

@umair-ably umair-ably commented Mar 12, 2026

Used #126 as a reference PR but heavily updated to follow new standards in the codebase

Summary by CodeRabbit

  • New Features

    • Added comprehensive push notification management with new commands for publishing notifications, managing device registrations, configuring APNs and FCM, and managing channel subscriptions.
    • Push batch publish functionality for sending up to 10,000 notifications at once.
  • Deprecations

    • Deprecated set-apns-p12 command; use ably push config set-apns instead.
  • Documentation

    • Updated README with complete push command reference.

Summary

Adds comprehensive push notification management to the Ably CLI, covering the full Push Admin API surface: device registrations, channel subscriptions, publishing (single + batch), and provider configuration (APNs/FCM) via the Control API. Also deprecates the legacy apps set-apns-p12 command in favour of push config set-apns.

51 files changed, ~6,700 lines added

What's new

  • ably push publish — Send a push notification to a single device or client
  • ably push batch-publish — Send push notifications to multiple recipients (JSON, file, or stdin)
  • ably push devices {get,list,save,remove,remove-where} — Manage device registrations
  • ably push channels {list,list-channels,save,remove,remove-where} — Manage channel subscriptions
  • ably push config {show,set-apns,set-fcm,clear-apns,clear-fcm} — Configure APNs/FCM via Control API
  • ably apps set-apns-p12 — Now hidden + deprecated with a warning pointing to push config set-apns

Other changes

  • base-command.ts: Restricts push* commands for anonymous web CLI users
  • control-api.ts: Expands App model with push-related fields; broadens updateApp to accept Partial<App>
  • output.ts: Adds formatDeviceState() helper
  • Test fixtures for APNs P8 key and FCM service account validation

Review strategy

This PR is large but highly repetitive — most push commands follow identical patterns. Reviewing in the order below lets you establish the patterns early and skim the rest.

Order Files What to look for Effort
1 src/base-command.ts, src/services/control-api.ts, src/utils/output.ts Core changes: web CLI restriction, App model expansion, new output helper. Small diffs, high impact. ~5 min
2 src/commands/push/publish.ts + test/unit/commands/push/publish.test.ts Read this carefully — establishes the pattern for all product API push commands (flags, error handling, JSON output, fail() usage). All other product API commands follow this template. ~10 min
3 src/commands/push/config/show.ts + test/unit/commands/push/config/show.test.ts Read this carefully — establishes the pattern for Control API push commands (ControlBaseCommand, runControlCommand, requireAppId). All other config commands follow this template. ~10 min
4 src/commands/push/devices/save.ts + test Most complex single command (many flags, conditional logic for platform-specific fields). Worth a close read. ~10 min
5 src/commands/push/batch-publish.ts + test Unique pattern: JSON/file/stdin input parsing. ~5 min
6 src/commands/push/config/set-apns.ts, set-fcm.ts + tests File reading + validation patterns (P8/P12/FCM service account). ~5 min
7 Remaining src/commands/push/devices/* + tests Skim — all follow the publish.ts pattern from step 2. Spot-check one remove and one list. ~5 min
8 Remaining src/commands/push/channels/* + tests Skim — identical structure to devices commands. ~3 min
9 Remaining src/commands/push/config/clear-* + tests Skim — confirmation prompt + Control API call. ~3 min
10 src/commands/apps/set-apns-p12.ts Tiny change: hidden = true + deprecation warning. ~1 min
11 test/helpers/mock-ably-rest.ts, test/fixtures/push/*, test/e2e/push/* Test infrastructure. Mock expansions and E2E coverage for push config. ~5 min
12 README.md, docs/Project-Structure.md, topic index.ts files Generated/boilerplate docs. Skim or skip. ~2 min

Test plan

  • pnpm test:unit — all 14 new push unit test files pass
  • pnpm test:e2e — 4 push E2E test files pass against real Ably
  • pnpm exec eslint . — 0 errors
  • Verify ably push --help shows all subcommand groups
  • Verify ably apps set-apns-p12 --help shows deprecation warning
  • Verify --json output produces correct NDJSON envelope for push commands

@vercel
Copy link

vercel bot commented Mar 12, 2026

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

Project Deployment Actions Updated (UTC)
cli-web-cli Ready Ready Preview, Comment Mar 14, 2026 2:56pm

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 174d6d29-dbc1-49b4-a636-7d59d90c243c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This pull request introduces a comprehensive push notification management system to the CLI, including device registration commands, channel subscription management, APNs and FCM configuration, and push publishing capabilities. The deprecated set-apns-p12 command is hidden with a migration warning. Service layer updates extend the control API App interface with push-related fields, and extensive unit and end-to-end test suites provide coverage for all new commands.

Changes

Cohort / File(s) Summary
Push Device Management
src/commands/push/devices/index.ts, src/commands/push/devices/get.ts, src/commands/push/devices/list.ts, src/commands/push/devices/remove.ts, src/commands/push/devices/remove-where.ts, src/commands/push/devices/save.ts
New commands for managing push device registrations: list with filtering, get by ID, save/update device data with platform and transport validation, remove individual device, and batch remove by filter criteria.
Push Channel Management
src/commands/push/channels/index.ts, src/commands/push/channels/list.ts, src/commands/push/channels/list-channels.ts, src/commands/push/channels/remove.ts, src/commands/push/channels/remove-where.ts, src/commands/push/channels/save.ts
New commands for managing push channel subscriptions: list subscriptions by channel/device/client, list all channels with subscriptions, subscribe device/client to channel, and remove subscriptions individually or by filter.
Push Configuration
src/commands/push/config/index.ts, src/commands/push/config/show.ts, src/commands/push/config/set-apns.ts, src/commands/push/config/set-fcm.ts, src/commands/push/config/clear-apns.ts, src/commands/push/config/clear-fcm.ts
New commands for APNs and FCM configuration: show current config state, set APNs via P12 certificate or P8 key, set FCM via service account JSON, and clear configurations with optional confirmation.
Push Publishing
src/commands/push/index.ts, src/commands/push/publish.ts, src/commands/push/batch-publish.ts
New commands for publishing push notifications: single publish to device/client, and batch publish with JSON array validation (max 10,000 items, per-item success/failure reporting).
Service Layer & Utilities
src/base-command.ts, src/services/control-api.ts, src/utils/output.ts
Extended control API App interface with APNS fields (authType, certificate, signingKey, etc.) and FCM serviceAccount; updated updateApp signature to accept Partial<App>; added formatDeviceState utility for colored state output.
Deprecation
src/commands/apps/set-apns-p12.ts
Marked command as hidden and added runtime deprecation warning directing users to "ably push config set-apns".
Documentation
README.md, docs/Project-Structure.md
Comprehensive push command documentation in README with usage examples; updated project structure docs to include push-related directory hierarchy.
Test Fixtures
test/fixtures/push/test-apns-key.p8, test/fixtures/push/test-fcm-service-account.json, test/helpers/mock-ably-rest.ts
Added test fixtures for APNs P8 key and FCM service account; extended mock Ably REST client with listChannels and removeWhere endpoints for channelSubscriptions and deviceRegistrations.
Unit Tests
test/unit/commands/push/.../*.test.ts (18 files)
Comprehensive unit tests covering all new push commands with standard scaffolding (help, validation, flags), functionality scenarios, JSON output formatting, and error handling.
E2E Tests
test/e2e/push/push-config-e2e.test.ts
End-to-end test suite validating push configuration workflows: APNs P8 setup/clear, FCM setup/clear, JSON/text output, error validation, and graceful skipping without E2E token.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A warren of commands now hops and bounds,
Push notifications across all grounds,
Devices subscribe, channels align,
APNs and FCM in perfect design,
The CLI toolkit grows strong and bright!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'DX-919 Extends push support' is directly related to the main changeset, which adds comprehensive push notification functionality including device management, channel subscriptions, publishing, and configuration commands.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch DX-919-push
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can disable the changed files summary in the walkthrough.

Disable the reviews.changed_files_summary setting to disable the changed files summary in the walkthrough.

@umair-ably
Copy link
Contributor Author

@coderabbitai review

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds new CLI capabilities for Ably Push Notifications (publish, batch publish, device registrations, channel subscriptions, and push config via Control API) and extends Chat room message management with update/delete commands, with accompanying unit/E2E tests and documentation updates.

Changes:

  • Introduces a new ably push ... command group (publish, batch-publish, devices, channels, config show/set/clear).
  • Adds ably rooms messages update and ably rooms messages delete Chat commands.
  • Updates Control API app model/update method, test mocks/fixtures, and CLI docs; deprecates/hides apps set-apns-p12.

Reviewed changes

Copilot reviewed 48 out of 48 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/unit/commands/rooms/messages/update.test.ts Unit tests for rooms:messages:update (flags, JSON output, errors).
test/unit/commands/rooms/messages/delete.test.ts Unit tests for rooms:messages:delete (details, JSON output, errors).
test/unit/commands/push/publish.test.ts Unit tests for push:publish.
test/unit/commands/push/devices/save.test.ts Unit tests for push:devices:save.
test/unit/commands/push/devices/remove.test.ts Unit tests for push:devices:remove.
test/unit/commands/push/devices/remove-where.test.ts Unit tests for push:devices:remove-where.
test/unit/commands/push/devices/list.test.ts Unit tests for push:devices:list.
test/unit/commands/push/devices/get.test.ts Unit tests for push:devices:get (token redaction).
test/unit/commands/push/config/show.test.ts Unit tests for push:config:show Control API rendering/JSON.
test/unit/commands/push/config/set-fcm.test.ts Unit tests for push:config:set-fcm (file validation).
test/unit/commands/push/config/set-apns.test.ts Unit tests for push:config:set-apns (p8/p12 flows).
test/unit/commands/push/config/clear-fcm.test.ts Unit tests for push:config:clear-fcm.
test/unit/commands/push/config/clear-apns.test.ts Unit tests for push:config:clear-apns.
test/unit/commands/push/channels/save.test.ts Unit tests for push:channels:save.
test/unit/commands/push/channels/remove.test.ts Unit tests for push:channels:remove.
test/unit/commands/push/channels/remove-where.test.ts Unit tests for push:channels:remove-where.
test/unit/commands/push/channels/list.test.ts Unit tests for push:channels:list.
test/unit/commands/push/channels/list-channels.test.ts Unit tests for push:channels:list-channels.
test/unit/commands/push/batch-publish.test.ts Unit tests for push:batch-publish.
test/helpers/mock-ably-rest.ts Extends REST mock to support new push admin APIs.
test/helpers/mock-ably-chat.ts Extends Chat mock to support message update/delete.
test/fixtures/push/test-fcm-service-account.json Test fixture for FCM service account validation.
test/fixtures/push/test-apns-key.p8 Test fixture for APNs P8 key flows.
test/e2e/push/push-config-e2e.test.ts E2E coverage for push config show/set/clear flows.
src/utils/output.ts Adds formatDeviceState helper for push device state display.
src/services/control-api.ts Expands App model fields; broadens updateApp to Partial<App>.
src/commands/rooms/typing/subscribe.ts Ensures early-return on fail() for missing Chat client.
src/commands/rooms/typing/keystroke.ts Ensures early-return on fail() for missing Chat client.
src/commands/rooms/reactions/subscribe.ts Ensures early-return on fail() for missing Chat client.
src/commands/rooms/reactions/send.ts Ensures early-return on fail() for missing Chat client.
src/commands/rooms/occupancy/subscribe.ts Ensures early-return on fail() for missing Chat client.
src/commands/rooms/occupancy/get.ts Ensures early-return on fail() for missing Chat client.
src/commands/rooms/messages/update.ts Implements message update command (metadata/headers/details/JSON output).
src/commands/rooms/messages/send.ts Ensures early-return on fail() for missing Chat client.
src/commands/rooms/messages/reactions/subscribe.ts Ensures early-return on fail() for missing Chat client.
src/commands/rooms/messages/reactions/send.ts Ensures early-return on fail() for missing Chat client.
src/commands/rooms/messages/reactions/remove.ts Ensures early-return on fail() for missing Chat client.
src/commands/rooms/messages/index.ts Adds examples for new update/delete subcommands.
src/commands/rooms/messages/history.ts Ensures early-return on fail() for missing Chat client.
src/commands/rooms/messages/delete.ts Implements message delete command (details/JSON output).
src/commands/push/publish.ts Implements single-recipient push publish command.
src/commands/push/index.ts Adds push top-level topic command.
src/commands/push/devices/save.ts Implements device registration save/update.
src/commands/push/devices/remove.ts Implements device registration removal with confirmation.
src/commands/push/devices/remove-where.ts Implements filtered bulk device removal with confirmation.
src/commands/push/devices/list.ts Implements device registration listing with state formatting.
src/commands/push/devices/index.ts Adds push devices topic command.
src/commands/push/devices/get.ts Implements device registration retrieval with token redaction.
src/commands/push/config/show.ts Implements push config display via Control API.
src/commands/push/config/set-fcm.ts Implements FCM configuration via service account file.
src/commands/push/config/set-apns.ts Implements APNs configuration via p12 upload or p8 key update.
src/commands/push/config/index.ts Adds push config topic command.
src/commands/push/config/clear-fcm.ts Implements clearing FCM config with confirmation and no-op behavior.
src/commands/push/config/clear-apns.ts Implements clearing APNs config with confirmation and no-op behavior.
src/commands/push/channels/save.ts Implements push channel subscription save.
src/commands/push/channels/remove.ts Implements push channel subscription removal with confirmation.
src/commands/push/channels/remove-where.ts Implements filtered bulk channel unsubscription with confirmation.
src/commands/push/channels/list.ts Implements listing channel subscriptions with filters/limit.
src/commands/push/channels/list-channels.ts Implements listing channels that have push subscriptions.
src/commands/push/channels/index.ts Adds push channels topic command.
src/commands/push/batch-publish.ts Implements batch publish with JSON/file/stdin input and validation.
src/commands/apps/set-apns-p12.ts Marks legacy APNs P12 command hidden and warns it’s deprecated.
src/base-command.ts Restricts anonymous web CLI usage for push* commands.
README.md Adds docs for new push commands and chat message update/delete; removes legacy APNs P12 docs.
docs/Project-Structure.md Documents new push command/test structure and fixtures.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (10)
test/unit/commands/push/devices/get.test.ts (1)

57-66: Use captureJsonLogs() instead of parsing raw stdout directly.

It’s more consistent with the unit-test JSON assertion pattern used across this repo.

♻️ Suggested test adjustment
 import {
   standardHelpTests,
   standardArgValidationTests,
   standardFlagTests,
 } from "../../../../helpers/standard-tests.js";
+import { captureJsonLogs } from "../../../../helpers/ndjson.js";
@@
-      const result = JSON.parse(stdout);
+      const [result] = captureJsonLogs(stdout);
       expect(result).toHaveProperty("type", "result");
       expect(result).toHaveProperty("success", true);
       expect(result).toHaveProperty("device");

Based on learnings: In unit tests for the ably/ably-cli repo, for JSON output assertions, use the captureJsonLogs() helper from test/helpers/ndjson.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/commands/push/devices/get.test.ts` around lines 57 - 66, The test
currently parses raw stdout from runCommand(["push:devices:get", "device-123",
"--json"], import.meta.url) and asserts on the parsed object; replace this with
the repository's JSON log helper by importing and using captureJsonLogs from
test/helpers/ndjson.ts to capture and parse the command output (wrap the
runCommand invocation with captureJsonLogs or call captureJsonLogs to execute
the command as per existing tests), then run the same assertions against the
returned parsed object (keep references to the runCommand invocation and the
"push:devices:get" args so the intent remains clear).
test/unit/commands/push/channels/list.test.ts (1)

64-73: Prefer captureJsonLogs() over direct JSON.parse(stdout) in unit JSON assertions.

This keeps assertions resilient to envelope/log formatting differences.

♻️ Suggested test adjustment
 import {
   standardHelpTests,
   standardArgValidationTests,
   standardFlagTests,
 } from "../../../../helpers/standard-tests.js";
+import { captureJsonLogs } from "../../../../helpers/ndjson.js";
@@
-      const result = JSON.parse(stdout);
+      const [result] = captureJsonLogs(stdout);
       expect(result).toHaveProperty("type", "result");
       expect(result).toHaveProperty("success", true);
       expect(result).toHaveProperty("subscriptions");

Based on learnings: In unit tests for the ably/ably-cli repo, for JSON output assertions, use the captureJsonLogs() helper from test/helpers/ndjson.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/commands/push/channels/list.test.ts` around lines 64 - 73, The test
currently uses JSON.parse(stdout) to assert JSON output; replace that with the
captureJsonLogs() helper from test/helpers/ndjson.ts to make assertions
resilient to envelope/log formatting. Call captureJsonLogs(stdout) (or import
and use captureJsonLogs around the runCommand call) to obtain the parsed JSON
object, then assert on properties like type, success, and subscriptions against
the returned object instead of using JSON.parse; update the assertions in the
test/unit/commands/push/channels/list.test.ts block where result is derived to
use captureJsonLogs.
src/services/control-api.ts (1)

11-30: Consider narrowing updateApp payload type for better type safety.

The use of Partial<App> is overly permissive because App includes an index signature ([key: string]: unknown). While current usage correctly passes only writable fields, explicitly defining which fields are updatable prevents accidental mutation of readonly fields in future changes.

♻️ Proposed type-safe patch shape
+export type AppUpdateData = Partial<
+  Pick<
+    App,
+    | "name"
+    | "tlsOnly"
+    | "apnsAuthType"
+    | "apnsCertificate"
+    | "apnsIssuerKey"
+    | "apnsPrivateKey"
+    | "apnsSigningKey"
+    | "apnsSigningKeyId"
+    | "apnsTopicHeader"
+    | "apnsUseSandboxEndpoint"
+    | "fcmServiceAccount"
+  >
+>;
+
 // Update an app
-async updateApp(appId: string, appData: Partial<App>): Promise<App> {
+async updateApp(appId: string, appData: AppUpdateData): Promise<App> {
   return this.request<App>(`/apps/${appId}`, "PATCH", appData);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/control-api.ts` around lines 11 - 30, The current update payload
uses Partial<App>, but because App has an index signature ([key: string]:
unknown) that makes Partial permissive, define a concrete update shape (e.g.,
type UpdateApp = { name?: string; status?: string; tlsOnly?: boolean;
apnsUseSandboxEndpoint?: boolean | null; apnsUsesSandboxCert?: boolean;
apnsAuthType?: string | null; apnsCertificate?: string | null; apnsIssuerKey?:
string | null; apnsPrivateKey?: string | null; apnsSigningKey?: string | null;
apnsSigningKeyId?: string | null; apnsTopicHeader?: string | null;
fcmServiceAccount?: string | null } or use Pick<App, 'name'|'status'|...>) and
replace all uses of Partial<App> in updateApp (and any related
functions/handlers) with this UpdateApp type to ensure only intended fields are
writable while preserving the original App interface.
test/unit/commands/push/config/show.test.ts (1)

159-170: Use captureJsonLogs() for --json assertions in this unit test.

Parsing raw stdout is more brittle than using the shared NDJSON capture helper used in this repo’s unit-test pattern.

Based on learnings: “For JSON output assertions, use the captureJsonLogs() helper from test/helpers/ndjson.ts.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/commands/push/config/show.test.ts` around lines 159 - 170, The test
currently parses raw stdout from runCommand for the --json output which is
brittle; replace that parsing by using the shared NDJSON helper
captureJsonLogs() to collect JSON results from runCommand (keep using runCommand
to execute ["push:config:show", "--json"]), then assert on the parsed object
returned by captureJsonLogs() (check properties type, success, appId, apns,
fcm). Update the test to call captureJsonLogs() after/around runCommand
invocation and use its returned JSON object for assertions instead of
JSON.parse(stdout).
test/unit/commands/push/publish.test.ts (1)

101-111: Prefer captureJsonLogs() over JSON.parse(stdout) in unit tests.

For JSON-mode assertions, use the shared NDJSON helper so tests remain stable even if non-JSON lines are emitted.

Based on learnings: “For JSON output assertions, use the captureJsonLogs() helper from test/helpers/ndjson.ts.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/commands/push/publish.test.ts` around lines 101 - 111, Replace the
direct JSON.parse(stdout) usage with the NDJSON helper: call captureJsonLogs()
to capture and parse JSON-mode output from runCommand (instead of parsing stdout
directly), then assert on the returned JSON object (checking "type", "success",
"published"); update the test around runCommand and the variable result to use
captureJsonLogs() to retrieve the parsed JSON log entry for assertions
(referencing captureJsonLogs and runCommand to locate the change).
src/commands/push/batch-publish.ts (2)

166-178: Consider adding a timeout for stdin reading.

The readStdin() method will block indefinitely if stdin never ends (e.g., user runs command without piping input). This is a minor UX concern rather than a bug.

♻️ Optional: Add timeout to prevent indefinite blocking
 private async readStdin(): Promise<string> {
-  return new Promise((resolve, reject) => {
+  return new Promise((resolve, reject) => {
+    const timeout = setTimeout(() => {
+      reject(new Error("Timeout waiting for stdin input"));
+    }, 30000); // 30 second timeout
+
     let data = "";
     process.stdin.setEncoding("utf8");
     process.stdin.on("data", (chunk) => {
       data += chunk;
     });
     process.stdin.on("end", () => {
+      clearTimeout(timeout);
       resolve(data);
     });
-    process.stdin.on("error", reject);
+    process.stdin.on("error", (err) => {
+      clearTimeout(timeout);
+      reject(err);
+    });
   });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/push/batch-publish.ts` around lines 166 - 178, The readStdin()
Promise can hang if stdin never ends; update the readStdin() method to include a
configurable timeout (e.g., default 5s) that rejects (or resolves with empty
string) when elapsed, and ensure you clean up listeners and the timer to avoid
leaks. Inside readStdin(), create a timer with setTimeout that triggers a
reject/resolve with a clear error message, and in each of the stdin event
handlers (data, end, error) clear the timer and remove their listeners; expose
the timeout as an optional parameter or constant so callers can adjust it if
needed.

59-76: Potential uninitialized variable after this.fail() calls.

TypeScript's control flow analysis may not recognize that this.fail() always throws, potentially flagging batchPayload as possibly uninitialized at line 70. Consider initializing with a dummy value or restructuring to make the control flow clearer.

♻️ Suggested restructure to clarify control flow
-      let batchPayload: unknown[];
-      try {
-        batchPayload = JSON.parse(jsonString) as unknown[];
-      } catch {
-        this.fail(
-          "Payload must be a valid JSON array",
-          flags as BaseFlags,
-          "pushBatchPublish",
-        );
-      }
-
-      if (!Array.isArray(batchPayload)) {
+      let batchPayload: unknown;
+      try {
+        batchPayload = JSON.parse(jsonString);
+      } catch {
+        return this.fail(
+          "Payload must be a valid JSON array",
+          flags as BaseFlags,
+          "pushBatchPublish",
+        );
+      }
+
+      if (!Array.isArray(batchPayload)) {
+        return this.fail(
+          "Payload must be a JSON array",
+          flags as BaseFlags,
+          "pushBatchPublish",
+        );
+      }

Adding return before this.fail() calls makes it explicit to TypeScript that these are terminal branches, even though fail() throws internally.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/push/batch-publish.ts` around lines 59 - 76, TypeScript may
think batchPayload is uninitialized after the error branches; update the error
branches in the JSON parsing/validation logic (the calls to this.fail(...)
inside the try/catch and the Array.isArray check in the pushBatchPublish
command) to explicitly return after calling this.fail (or alternatively
initialize batchPayload to a safe default like [] as unknown[]) so the compiler
understands those branches are terminal and batchPayload is always assigned
before use.
src/commands/push/channels/save.ts (1)

67-67: Consider using proper typing instead of as never cast.

The as never cast suppresses type checking entirely. If the SDK expects a specific type for the subscription parameter, consider defining or importing that type for better type safety.

♻️ Suggested improvement
-      await rest.push.admin.channelSubscriptions.save(subscription as never);
+      await rest.push.admin.channelSubscriptions.save(
+        subscription as { channel: string; deviceId?: string; clientId?: string },
+      );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/push/channels/save.ts` at line 67, The call to
rest.push.admin.channelSubscriptions.save currently uses a blanket cast "as
never" which disables type checking; replace this with the correct SDK type for
the subscription parameter by importing or declaring the expected type (e.g.,
the type the SDK exposes for channel subscription payload) and change the call
to rest.push.admin.channelSubscriptions.save(subscription) typed as that type
(or cast to that specific SDK type) so TypeScript can validate the payload;
locate the call site using the save method and the subscription variable to
implement the typed change.
src/commands/push/devices/get.ts (1)

47-50: Consider redacting deviceToken in JSON output as well.

The deviceToken is redacted in human-readable output (lines 70-77) but not in JSON output. If this is intentional for automation use cases, consider documenting this behavior. Otherwise, sensitive tokens should be consistently redacted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/push/devices/get.ts` around lines 47 - 50, The JSON branch
currently calls logJsonResult({ device }, flags) without redacting
device.deviceToken; update the logic in the shouldOutputJson path to redact the
token the same way as the human-readable branch (apply the same redaction logic
used around the human output code to device.deviceToken) before calling
logJsonResult so JSON output does not expose the raw token (or document
intentional exception if you want to keep raw tokens for automation). Reference
symbols: shouldOutputJson, logJsonResult, and device.deviceToken to locate where
to mutate the object prior to logging.
src/commands/push/config/show.ts (1)

80-129: Use the shared section-output helpers here.

These raw section headers and the final guidance line won't match the rest of the CLI's human-readable output. Use formatHeading() / formatWarning() instead of plain strings.

As per coding guidelines "Always use formatProgress(), formatSuccess(), formatWarning(), formatListening(), formatResource(), formatTimestamp(), formatClientId(), formatEventType(), formatHeading(), formatIndex(), formatCountLabel(), and formatLimitWarning() from src/utils/output.ts instead of direct chalk calls".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/push/config/show.ts` around lines 80 - 129, The output uses raw
strings for section headers and the final guidance line; replace those with the
shared helpers to match CLI style: use formatHeading() for the section titles
("APNs Configuration:", "FCM Configuration:", "Web Push:") instead of
this.log("..."), keep the existing formatSuccess/formatResource usage, and use
formatWarning() for the final guidance line ("Configure APNs or FCM to enable
web push notifications."); ensure all calls still go through this.log(...) and
preserve the conditional blocks around apnsConfigured/fcm/projectId as-is so
only the presentation changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 2665-3251: The README push docs contain many unannotated fenced
code blocks (e.g., the blocks under the USAGE/DESCRIPTION/EXAMPLES sections for
"ably push", "ably push batch-publish", "ably push channels", etc.) which
trigger markdownlint MD040; update each triple-backtick fence in that push docs
section to include a language identifier (for example use "text" or
"sh-session") so fences read ```text or ```sh-session consistently across the
blocks for commands like "ably push", "ably push batch-publish", "ably push
channels", "ably push config", and "ably push devices".

In `@src/commands/apps/set-apns-p12.ts`:
- Around line 52-54: Replace the unconditional this.warn call in the
set-apns-p12 command with a guarded, stderr-formatted warning: wrap the warning
in if (!this.shouldOutputJson(flags)) and inside that block call
this.logToStderr(this.formatWarning('This command is deprecated. Use "ably push
config set-apns" instead.')); reference the existing this.warn usage and the
flags variable in the command to locate where to change this behavior.

In `@src/commands/push/channels/list.ts`:
- Around line 73-75: Replace direct human-readable outputs in the channel list
command with the shared output formatters: where the code currently does
this.log("No subscriptions found.") use formatWarning(...) to render the
empty-state message, and where it prints raw clientId use formatClientId(...) to
render client identifiers; update the usages in the method handling
subscriptions (references: subscriptions array check and the later clientId
output) to call formatWarning and formatClientId from src/utils/output.ts and
pass the same message/string values to preserve behavior.

In `@src/commands/push/channels/save.ts`:
- Around line 41-47: The call to this.fail(...) in the push channel save logic
can return control instead of throwing in some environments; after the
validation block that checks flags (the snippet using flags, BaseFlags and the
"pushChannelSave" tag), add an immediate return right after this.fail(...) to
defensively stop execution when neither --device-id nor --client-id is provided
so subsequent code in the surrounding method does not run with invalid state.

In `@src/commands/push/config/set-apns.ts`:
- Around line 69-77: The code path that checks flags.certificate calls
this.fail(...) when the certificate file doesn't exist but doesn't return/stop
execution, so subsequent code (e.g., using certPath) can run; update the
pushConfigSetApns flow to immediately exit after this.fail by adding a return
(or throw) right after the this.fail(...) call so the function does not continue
when the cert file is missing — reference the flags.certificate check, certPath,
and this.fail() when making the change.
- Around line 56-62: The validation in the pushConfigSetApns flow calls
this.fail(...) but does not stop execution; add an explicit return immediately
after the this.fail(...) call in the code path inside pushConfigSetApns (the
block that checks flags.certificate and flags["key-file"]) so execution does not
continue when the validation fails; reference the flags variable used in that
check and ensure the function/method exits after calling this.fail().
- Around line 108-137: The validation branch in pushConfigSetApns calling
this.fail() for missing flags ("key-id", "team-id", "topic") and missing key
file (keyPath / fs.existsSync) does not stop execution; add an immediate
control-flow stop after each this.fail() call (e.g., return or throw) so the
function exits and does not continue into the P8 flow when validation fails —
update the checks around flags["key-id"], flags["team-id"], flags.topic and the
keyPath existence check to return immediately after calling this.fail() in the
pushConfigSetApns logic.

In `@src/commands/push/devices/save.ts`:
- Around line 210-237: parseDataFlag currently returns any JSON value
(arrays/primitives) but callers like run() expect a device object (e.g.,
accessing deviceData.id and passing to deviceRegistrations.save); update
parseDataFlag to validate the parsed JSON is a non-null plain object (typeof
result === "object" && result !== null && !Array.isArray(result)) and if not
call this.fail with the existing error message/flags/"pushDeviceSave" so only
object payloads are returned.

In `@src/commands/push/publish.ts`:
- Around line 95-179: The parsed JSON for structured flags in pushPublish must
be validated to ensure it's a non-null plain object; update each JSON.parse
block for flags.recipient, flags.payload (and the fallback-built payload),
flags.data (when parsed), flags.apns, flags.fcm, and flags.web to check typeof
parsed === "object" && parsed !== null && !Array.isArray(parsed) and call
this.fail("--<flag> must be a JSON object", flags as BaseFlags, "pushPublish")
when the check fails; ensure payload remains a Record<string, unknown> and that
the payload = JSON.parse(...) assignments are replaced with the validated object
before use.

---

Nitpick comments:
In `@src/commands/push/batch-publish.ts`:
- Around line 166-178: The readStdin() Promise can hang if stdin never ends;
update the readStdin() method to include a configurable timeout (e.g., default
5s) that rejects (or resolves with empty string) when elapsed, and ensure you
clean up listeners and the timer to avoid leaks. Inside readStdin(), create a
timer with setTimeout that triggers a reject/resolve with a clear error message,
and in each of the stdin event handlers (data, end, error) clear the timer and
remove their listeners; expose the timeout as an optional parameter or constant
so callers can adjust it if needed.
- Around line 59-76: TypeScript may think batchPayload is uninitialized after
the error branches; update the error branches in the JSON parsing/validation
logic (the calls to this.fail(...) inside the try/catch and the Array.isArray
check in the pushBatchPublish command) to explicitly return after calling
this.fail (or alternatively initialize batchPayload to a safe default like [] as
unknown[]) so the compiler understands those branches are terminal and
batchPayload is always assigned before use.

In `@src/commands/push/channels/save.ts`:
- Line 67: The call to rest.push.admin.channelSubscriptions.save currently uses
a blanket cast "as never" which disables type checking; replace this with the
correct SDK type for the subscription parameter by importing or declaring the
expected type (e.g., the type the SDK exposes for channel subscription payload)
and change the call to rest.push.admin.channelSubscriptions.save(subscription)
typed as that type (or cast to that specific SDK type) so TypeScript can
validate the payload; locate the call site using the save method and the
subscription variable to implement the typed change.

In `@src/commands/push/config/show.ts`:
- Around line 80-129: The output uses raw strings for section headers and the
final guidance line; replace those with the shared helpers to match CLI style:
use formatHeading() for the section titles ("APNs Configuration:", "FCM
Configuration:", "Web Push:") instead of this.log("..."), keep the existing
formatSuccess/formatResource usage, and use formatWarning() for the final
guidance line ("Configure APNs or FCM to enable web push notifications.");
ensure all calls still go through this.log(...) and preserve the conditional
blocks around apnsConfigured/fcm/projectId as-is so only the presentation
changes.

In `@src/commands/push/devices/get.ts`:
- Around line 47-50: The JSON branch currently calls logJsonResult({ device },
flags) without redacting device.deviceToken; update the logic in the
shouldOutputJson path to redact the token the same way as the human-readable
branch (apply the same redaction logic used around the human output code to
device.deviceToken) before calling logJsonResult so JSON output does not expose
the raw token (or document intentional exception if you want to keep raw tokens
for automation). Reference symbols: shouldOutputJson, logJsonResult, and
device.deviceToken to locate where to mutate the object prior to logging.

In `@src/services/control-api.ts`:
- Around line 11-30: The current update payload uses Partial<App>, but because
App has an index signature ([key: string]: unknown) that makes Partial
permissive, define a concrete update shape (e.g., type UpdateApp = { name?:
string; status?: string; tlsOnly?: boolean; apnsUseSandboxEndpoint?: boolean |
null; apnsUsesSandboxCert?: boolean; apnsAuthType?: string | null;
apnsCertificate?: string | null; apnsIssuerKey?: string | null; apnsPrivateKey?:
string | null; apnsSigningKey?: string | null; apnsSigningKeyId?: string | null;
apnsTopicHeader?: string | null; fcmServiceAccount?: string | null } or use
Pick<App, 'name'|'status'|...>) and replace all uses of Partial<App> in
updateApp (and any related functions/handlers) with this UpdateApp type to
ensure only intended fields are writable while preserving the original App
interface.

In `@test/unit/commands/push/channels/list.test.ts`:
- Around line 64-73: The test currently uses JSON.parse(stdout) to assert JSON
output; replace that with the captureJsonLogs() helper from
test/helpers/ndjson.ts to make assertions resilient to envelope/log formatting.
Call captureJsonLogs(stdout) (or import and use captureJsonLogs around the
runCommand call) to obtain the parsed JSON object, then assert on properties
like type, success, and subscriptions against the returned object instead of
using JSON.parse; update the assertions in the
test/unit/commands/push/channels/list.test.ts block where result is derived to
use captureJsonLogs.

In `@test/unit/commands/push/config/show.test.ts`:
- Around line 159-170: The test currently parses raw stdout from runCommand for
the --json output which is brittle; replace that parsing by using the shared
NDJSON helper captureJsonLogs() to collect JSON results from runCommand (keep
using runCommand to execute ["push:config:show", "--json"]), then assert on the
parsed object returned by captureJsonLogs() (check properties type, success,
appId, apns, fcm). Update the test to call captureJsonLogs() after/around
runCommand invocation and use its returned JSON object for assertions instead of
JSON.parse(stdout).

In `@test/unit/commands/push/devices/get.test.ts`:
- Around line 57-66: The test currently parses raw stdout from
runCommand(["push:devices:get", "device-123", "--json"], import.meta.url) and
asserts on the parsed object; replace this with the repository's JSON log helper
by importing and using captureJsonLogs from test/helpers/ndjson.ts to capture
and parse the command output (wrap the runCommand invocation with
captureJsonLogs or call captureJsonLogs to execute the command as per existing
tests), then run the same assertions against the returned parsed object (keep
references to the runCommand invocation and the "push:devices:get" args so the
intent remains clear).

In `@test/unit/commands/push/publish.test.ts`:
- Around line 101-111: Replace the direct JSON.parse(stdout) usage with the
NDJSON helper: call captureJsonLogs() to capture and parse JSON-mode output from
runCommand (instead of parsing stdout directly), then assert on the returned
JSON object (checking "type", "success", "published"); update the test around
runCommand and the variable result to use captureJsonLogs() to retrieve the
parsed JSON log entry for assertions (referencing captureJsonLogs and runCommand
to locate the change).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d07b42f2-c91a-46c2-8b5b-f92c374645a6

📥 Commits

Reviewing files that changed from the base of the PR and between fc7c4c6 and abf3ff4.

📒 Files selected for processing (48)
  • README.md
  • docs/Project-Structure.md
  • src/base-command.ts
  • src/commands/apps/set-apns-p12.ts
  • src/commands/push/batch-publish.ts
  • src/commands/push/channels/index.ts
  • src/commands/push/channels/list-channels.ts
  • src/commands/push/channels/list.ts
  • src/commands/push/channels/remove-where.ts
  • src/commands/push/channels/remove.ts
  • src/commands/push/channels/save.ts
  • src/commands/push/config/clear-apns.ts
  • src/commands/push/config/clear-fcm.ts
  • src/commands/push/config/index.ts
  • src/commands/push/config/set-apns.ts
  • src/commands/push/config/set-fcm.ts
  • src/commands/push/config/show.ts
  • src/commands/push/devices/get.ts
  • src/commands/push/devices/index.ts
  • src/commands/push/devices/list.ts
  • src/commands/push/devices/remove-where.ts
  • src/commands/push/devices/remove.ts
  • src/commands/push/devices/save.ts
  • src/commands/push/index.ts
  • src/commands/push/publish.ts
  • src/services/control-api.ts
  • src/utils/output.ts
  • test/e2e/push/push-config-e2e.test.ts
  • test/fixtures/push/test-apns-key.p8
  • test/fixtures/push/test-fcm-service-account.json
  • test/helpers/mock-ably-rest.ts
  • test/unit/commands/push/batch-publish.test.ts
  • test/unit/commands/push/channels/list-channels.test.ts
  • test/unit/commands/push/channels/list.test.ts
  • test/unit/commands/push/channels/remove-where.test.ts
  • test/unit/commands/push/channels/remove.test.ts
  • test/unit/commands/push/channels/save.test.ts
  • test/unit/commands/push/config/clear-apns.test.ts
  • test/unit/commands/push/config/clear-fcm.test.ts
  • test/unit/commands/push/config/set-apns.test.ts
  • test/unit/commands/push/config/set-fcm.test.ts
  • test/unit/commands/push/config/show.test.ts
  • test/unit/commands/push/devices/get.test.ts
  • test/unit/commands/push/devices/list.test.ts
  • test/unit/commands/push/devices/remove-where.test.ts
  • test/unit/commands/push/devices/remove.test.ts
  • test/unit/commands/push/devices/save.test.ts
  • test/unit/commands/push/publish.test.ts

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 53 out of 53 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Guard deprecation warning in set-apns-p12 for JSON mode
- Use formatWarning() and formatClientId() in push channels list
- Validate --data flag parses to a JSON object in push devices save
- Validate all JSON flags (--recipient, --payload, --data, --apns, --fcm, --web) parse to objects in push publish
- Let E2E test setup failures propagate instead of silently skipping
Three bugs found during manual testing of push publish commands:

1. `push publish --payload <file>` failed with "--payload must be a valid
   JSON object" because the command only accepted inline JSON strings.
   Added file path support: both `@filepath` (curl-style) and bare paths
   starting with `./`, `../`, or `/` are now resolved and read from disk.

2. `push batch-publish --payload <file>` required the undocumented `@`
   prefix (e.g. `@./file.json`). Bare path support added here too,
   consistent with the fix above.

3. `push batch-publish` validator checked `entry.notification` and
   `entry.data` at the top level of each batch item, but the Ably batch
   publish API uses `{recipient, payload: {notification, data}}` — the
   notification content is nested under `payload`. This caused all
   batch-publish calls with correctly-formatted payloads to be rejected
   with "Item at index 0 must have a 'notification' or 'data' field".
   Validator updated to check `entry.payload.notification` /
   `entry.payload.data`. The inline example in the command was also wrong
   (missing `payload` wrapper) and has been corrected.

Tests updated to use the correct `{recipient, payload: {notification}}`
batch item format.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@maratal
Copy link
Contributor

maratal commented Mar 14, 2026

@umair-ably Manual testing results — 3 bugs found and fixed in commit 14ce8a3.


Test commands (✅ passed, ❌ failed)

✅  push publish   --device-id test-device-1 --title "Hello" --body "World 001"
✅  push publish   --device-id test-device-1 --title "Hello" --body "World 002" --data '{"key":"value"}'
✅  push publish   --device-id test-device-1 --payload '{"notification":{"title":"Hello","body":"World 003"}}'
❌  push publish   --device-id test-device-1 --payload ./notification.json
✅  push publish   --client-id push-tutorial-client --title "Hello" --body "World 005"
✅  push publish   --client-id push-tutorial-client --payload '{"notification":{"title":"Hello","body":"World 006"}}'
❌  push batch-publish --payload '[{"recipient":{"deviceId":"test-device-1"},"payload":{"notification":{"title":"Hello","body":"World 007"}}}]'
❌  push batch-publish --payload ./notifications.json

Bug details

Bug 1 — push publish --payload ./notification.json (push 004)
publish.ts passed flags.payload directly to JSON.parse() with no file path handling. JSON.parse("./notification.json") throws → --payload must be a valid JSON object.

Fix: added @filepath (curl-style) and bare path (./, ../, /) support, matching how batch-publish handles files.


Bug 2 — push batch-publish validator rejected correctly-formatted payloads (push 007)
batch-publish.ts validated entry.notification and entry.data at the top level of each batch item. But the Ably REST API (POST /push/batch/publish) uses {recipient, payload: {notification, data}} — the notification content is nested under payload. Confirmed against https://ably.com/docs/api/rest-api#batch-push-publish.

All correctly-formatted batch payloads were rejected with:

Item at index 0 must have a "notification" or "data" field

Fix: validator now checks entry.payload.notification / entry.payload.data. The inline example in the command was also using the wrong flat format and has been corrected.


Bug 3 — push batch-publish --payload ./notifications.json (push 008)
batch-publish.ts only supported the undocumented @filepath curl-style prefix for file paths. Passing ./notifications.json without @ fell through to JSON.parse()Payload must be a valid JSON array.

Fix: bare path detection added (same as Bug 1 fix in publish).

The uploadApnsP12 method was calling POST /apps/{appId}/push/certificate
which returns 404. The correct path per the Control API OpenAPI spec is
POST /apps/{appId}/pkcs12.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@maratal
Copy link
Contributor

maratal commented Mar 14, 2026

@umair-ably Follow-up: manual testing of push config commands. All 5 commands were broken — bugs and fixes detailed below.


Commands tested

Command Result Root cause
push config set-fcm ✅ Works PATCH /apps/{id} with fcmServiceAccount is valid per app_patch schema — earlier failure was a bad access token
push config set-apns --certificate ❌ → ✅ fixed Wrong content-type + wrong endpoint path + wrong field names
push config set-apns --key-file ❌ Not fixable Token-based APNs P8 fields are not in app_patch schema; no dedicated endpoint exists in the Control API
push config clear-fcm ❌ Always skips getApp() uses GET /apps list which never returns fcmServiceAccount → check always false → "nothing to clear"
push config clear-apns ❌ Always skips + would 400 Same getApp() issue + PATCH sends invalid fields (apnsAuthType, apnsIssuerKey, apnsSigningKey, apnsSigningKeyId, apnsTopicHeader) not in app_patch
push config show ❌ Always "Not configured" app_response schema omits all push fields; no GET /apps/{id} endpoint exists

Bug 1 — push config set-apns --certificate (fixed in 3c926f1)

Three separate issues:

a) Wrong endpoint path — code called POST /apps/{id}/push/certificate → 404. Correct path per OpenAPI spec: POST /apps/{id}/pkcs12.

b) Wrong content type — method was sending base64-encoded JSON { p12Certificate, password }. The /pkcs12 endpoint requires multipart/form-data with fields p12File (binary) and p12Pass (string), as defined by the app_pkcs12 schema.

c) Sandbox flaguseForSandbox is not part of the app_pkcs12 schema (additionalProperties: false). Fixed by making a follow-up PATCH /apps/{id} with { apnsUseSandboxEndpoint } when --sandbox is set — that field IS in app_patch.


Bug 2 — push config set-apns --key-file (not fixable with current API)

The code sends these fields via PATCH /apps/{id}:

apnsAuthType, apnsIssuerKey, apnsSigningKey, apnsSigningKeyId, apnsTopicHeader

None of these are in the app_patch schema → 400 Bad Request.

The app_patch schema only supports certificate-based APNs (apnsCertificate, apnsPrivateKey). Token-based APNs (P8 key) has no Control API support. This needs either a new endpoint or additions to the app_patch schema from the backend.


Bug 3 — push config clear-fcm, push config clear-apns, push config show (not fixed — requires API changes)

All three share the same root cause: getApp() is implemented as:

const apps = await this.listApps();  // GET /apps
return apps.find((a) => a.id === appId);

GET /apps returns app_response which only contains: id, name, status, tlsOnly, apnsUseSandboxEndpoint. No push fields (fcmServiceAccount, apnsCertificate, etc.) are returned.

  • clear-fcm: fcmConfigured = !!app.fcmServiceAccount → always false → always prints "nothing to clear" without attempting the PATCH
  • clear-apns: same check issue + the PATCH sends invalid fields (apnsAuthType etc.) that would 400 if it ran
  • show: all push field checks are always false → always shows "Not configured"

There is no GET /apps/{id} endpoint in the Control API. These commands need either a dedicated GET /apps/{id}/push endpoint, or push fields need to be included in app_response.


Source: Control API OpenAPI spec

maratal and others added 2 commits March 14, 2026 13:44
The uploadApnsP12 method had three bugs found during manual testing:

1. Wrong content type: the method was sending the certificate as
   base64-encoded JSON, but POST /apps/{id}/pkcs12 requires
   multipart/form-data with fields p12File (binary) and p12Pass
   (string), as defined in the Control API OpenAPI spec.

2. Wrong field names: the original code sent { p12Certificate, password,
   useForSandbox } but the API schema defines { p12File, p12Pass } only.
   useForSandbox is not part of the pkcs12 endpoint schema at all.

3. Sandbox flag handling: since useForSandbox cannot be sent to the
   pkcs12 endpoint, uploadApnsP12 now handles it as a follow-up
   PATCH /apps/{id} with { apnsUseSandboxEndpoint } when the flag is set.
   This is the correct endpoint for that field per the app_patch schema.

Updated callers (push config set-apns, apps set-apns-p12) to pass a
Buffer instead of a base64 string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dbox flag

The condition `useForSandbox !== undefined` was always true because both
`--use-for-sandbox` (apps:set-apns-p12) and `--sandbox` (push:config:set-apns)
default to `false` rather than `undefined`. This caused every P12 upload to
make an extra PATCH /apps/{id} request even when sandbox mode wasn't requested,
breaking nock intercepts in tests and sending unnecessary API calls in production.

Fix: change condition to truthy check (`if (options.useForSandbox)`) so the
sandbox PATCH is only made when explicitly set to true.

Also add missing nock interceptor for the PATCH in the sandbox test case of
apps:set-apns-p12, and fix nock path from /push/certificate to /pkcs12 in
both test files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ormat

Ably batch push API requires {recipient, payload: {notification}} not
the flat {recipient, notification} format used in these tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Control API stores fcmProjectId as a separate field from
fcmServiceAccount. Not sending it meant the dashboard kept showing
the old project ID after updating the service account.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

3 participants