command_tower 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/command_tower/me/push_controller.rb +66 -0
  3. data/app/deserializers/command_tower/deserializers/me/push/token_deserializer.rb +42 -0
  4. data/app/errors/command_tower/errors/account/push_capability_unavailable_error.rb +21 -0
  5. data/app/errors/command_tower/errors/account/push_endpoint_not_found_error.rb +21 -0
  6. data/app/serializers/command_tower/serializers/me/push_serializer.rb +47 -0
  7. data/app/services/command_tower/messaging/channel_detectors.rb +6 -0
  8. data/app/services/command_tower/messaging/endpoints/validators/push.rb +6 -0
  9. data/app/services/command_tower/messaging/execution/adapter_request.rb +21 -3
  10. data/app/services/command_tower/messaging/execution/adapters/expo/adapter.rb +229 -0
  11. data/app/services/command_tower/messaging/execution/adapters/expo/configuration.rb +42 -0
  12. data/app/services/command_tower/messaging/execution/adapters/expo/http_client.rb +137 -0
  13. data/app/services/command_tower/messaging/execution/resolve_recipient_address.rb +7 -0
  14. data/app/services/command_tower/messaging/execution/select_adapter.rb +4 -0
  15. data/app/services/command_tower/messaging/rendering/channel_renderer.rb +21 -3
  16. data/app/services/command_tower/messaging/rendering/rendered_push_payload.rb +50 -0
  17. data/app/services/command_tower/services/account/push/create.rb +25 -0
  18. data/app/services/command_tower/services/account/push/ct_support.rb +38 -0
  19. data/app/services/command_tower/services/account/push/destroy.rb +23 -0
  20. data/app/services/command_tower/services/account/push/list.rb +23 -0
  21. data/app/services/command_tower/services/account/push/replace.rb +26 -0
  22. data/app/services/command_tower/services/me/push_product_gate.rb +18 -0
  23. data/app/views/command_tower/messaging/rendering/push.text.erb +1 -0
  24. data/app/workflows/command_tower/workflows/me/error_mapping.rb +3 -1
  25. data/app/workflows/command_tower/workflows/me/push/create_workflow.rb +37 -0
  26. data/app/workflows/command_tower/workflows/me/push/destroy_workflow.rb +37 -0
  27. data/app/workflows/command_tower/workflows/me/push/index_workflow.rb +34 -0
  28. data/app/workflows/command_tower/workflows/me/push/replace_workflow.rb +38 -0
  29. data/app/workflows/command_tower/workflows/me/push/workflow_support.rb +35 -0
  30. data/app/workflows/command_tower/workflows/messaging/execution/deliver_workflow.rb +1 -0
  31. data/config/routes.rb +9 -0
  32. data/docs/api_reference.md +21 -0
  33. data/docs/controllers.md +1 -0
  34. data/docs/messaging_integration_guide.md +8 -3
  35. data/docs/upgrades/0.14.0.md +31 -0
  36. data/docs/upgrades/README.md +1 -0
  37. data/lib/command_tower/authorization/default.yml +7 -0
  38. data/lib/command_tower/configuration/messaging/config.rb +6 -0
  39. data/lib/command_tower/configuration/messaging/expo.rb +38 -0
  40. data/lib/command_tower/version.rb +1 -1
  41. metadata +25 -2
@@ -426,6 +426,27 @@ Errors include `422` (`pushover_already_configured`, `pushover_not_configured`,
426
426
 
427
427
  ---
428
428
 
429
+ ## Push (Expo)
430
+
431
+ Routes are **always drawn**. Product readiness is workflow-gated as **503** `push_capability_unavailable` (`config.messaging.expo.adapter` must be `fake`, `log`, or `http`).
432
+
433
+ There is **no** `POST /me/push/verification`. Create and replace call `Endpoints.mark_verified` in-workflow after persist.
434
+
435
+ | Method | Path | Body | Notes |
436
+ |--------|------|------|--------|
437
+ | `GET` | `/me/push` | — | `{ endpoints: [...] }` — **active only** (terminal rows omitted) |
438
+ | `POST` | `/me/push` | `token`/`address` (snake or camelCase) | Create or idempotent same-fingerprint; returns verified endpoint |
439
+ | `PATCH` / `PUT` | `/me/push/:id` | same | Replace by id; new row verified; other actives untouched |
440
+ | `DELETE` | `/me/push/:id` | — | Revoke; returns revoked SafeView |
441
+
442
+ **Token form:** `ExponentPushToken[...]` or `ExpoPushToken[...]` (422 otherwise).
443
+
444
+ **Endpoint fields:** `id`, `channelKey`, `lifecycleState`, `verificationState`, `maskedDisplayValue`, `verifiedAt`, `createdAt`, `updatedAt`, `actions: { canReplace, canRemove }`. Never the raw token.
445
+
446
+ **RBAC:** `me_push`. **Spec:** `spec/requests/command_tower/me/push_spec.rb`.
447
+
448
+ ---
449
+
429
450
  ## Admin Workspace
430
451
 
431
452
  ### `GET /admin/workspace`
data/docs/controllers.md CHANGED
@@ -20,6 +20,7 @@ This page is an **index** of route areas. Detailed request/response contracts li
20
20
  | Preferences | `/me/preferences*` | Notification preferences |
21
21
  | Phone | `/me/phone*` | Phone endpoint + verification |
22
22
  | Pushover | `/me/pushover*` | Pushover endpoint lifecycle + verification |
23
+ | Push | `/me/push*` | Expo push endpoint collection (register / replace / revoke) |
23
24
  | Admin messaging | `/admin/messaging/announcements` | Cohort announcements |
24
25
 
25
26
  Exact paths depend on where the host mounts the engine.
@@ -10,7 +10,8 @@ Complete install and host RBAC first: [Host integration](host_integration_guide.
10
10
  |---------|----------------|
11
11
  | User inbox (consume) | Engine `/me/inbox*` — see [API reference](api_reference.md#me-inbox) |
12
12
  | Preferences | Engine `/me/preferences*` — see [API reference](api_reference.md#preferences) |
13
- | Phone / Pushover endpoints | Engine `/me/phone*`, `/me/pushover*` (503 when capability unavailable) |
13
+ | Phone / Pushover / Push endpoints | Engine `/me/phone*`, `/me/pushover*`, `/me/push*` (503 when capability unavailable) |
14
+
14
15
  | Single-recipient emit | `CommandTower::Services::Messaging::Communications::Produce` |
15
16
  | Multi-recipient emit | `CommandTower::Services::Messaging::Communications::ProduceMany` |
16
17
  | Admin cohort announce | Engine `POST /admin/messaging/announcements` |
@@ -73,10 +74,14 @@ Pagination detail: [pagination.md](pagination.md). Full catalog: [api_reference.
73
74
 
74
75
  - Notification catalog content (registered into CommandTower notification types)
75
76
  - `platform_enabled_channels` / channel policy injection
76
- - Messaging adapter credentials (email / SMS / Pushover) via initializer or ENV
77
- - Host product roles that grant CT-owned Me inbox/preferences/phone/pushover entities (and `admin` if used)
77
+ - Messaging adapter credentials (email / SMS / Pushover / Expo push) via initializer or ENV
78
+ - Host product roles that grant CT-owned Me inbox/preferences/phone/pushover/`me_push` entities (and `admin` if used)
78
79
  - Product-specific operational tooling (announce rake tasks, welcome copy)
79
80
 
81
+ Expo push (`config.messaging.expo`): set `adapter` to `fake`, `log`, or `http` to enable the Messaging `push` channel and Me `/me/push*` (default `disabled`). `access_token` is optional — send `Authorization: Bearer` only when non-blank (required only if the Expo project enables enhanced push security). Never log the token.
82
+
83
+ Me push registration: engine `/me/push*` (collection of active endpoints). Create/replace persist via `Endpoints` and **mark_verified** in-workflow — there is no `POST /me/push/verification`. Tokens must be Expo form (`ExponentPushToken[...]` / `ExpoPushToken[...]`).
84
+
80
85
  ## Related
81
86
 
82
87
  - [Controllers / routes](controllers.md)
@@ -0,0 +1,31 @@
1
+ # Upgrade: CommandTower 0.14.0
2
+
3
+ **From:** `0.13.1`
4
+ **To:** `0.14.0`
5
+
6
+ Minor release: Expo push as an executable Messaging channel, plus engine `/me/push*` registration HTTP. No schema migration.
7
+
8
+ ## Host-visible changes
9
+
10
+ | Change | Host impact |
11
+ |--------|-------------|
12
+ | `config.messaging.expo` | New composer: `adapter` (`disabled` / `fake` / `log` / `http`) and optional `access_token` (Bearer sent only when non-blank). Required for `push` to be platform-configured. |
13
+ | Channel `push` execution | Detector, renderer (`RenderedPushPayload`), Expo adapter (send + one getReceipts pass), multi-endpoint fan-out via `AdapterRequest.eligible_endpoint_ids`. `DeviceNotRegistered` → `Endpoints.mark_invalid`. |
14
+ | `/api/me/push*` | Collection register / list (active) / replace / revoke. Create/replace call `mark_verified` in-workflow (no verification POST). 503 when Expo adapter is disabled. |
15
+ | RBAC `me_push` | Show/create/update/destroy for Me push. Hosts must grant on roles that should register device tokens (e.g. `member`). |
16
+ | `Validators::Push` | Requires Expo token form (`ExponentPushToken[…]` / `ExpoPushToken[…]`). |
17
+
18
+ ## Host actions
19
+
20
+ 1. Bump gem to `0.14.0` (or `>= 0.14.0`).
21
+ 2. **No new migration.**
22
+ 3. Set `config.messaging.expo.adapter` (use `http` on production TF API; `fake` is fine for development/test with `allow_fake_adapter`).
23
+ 4. Optionally set `config.messaging.expo.access_token` **only if** the Expo project has enhanced push security enabled.
24
+ 5. Enable `push` in `platform_enabled_channels` and grant `me_push` when the host product should register/send push.
25
+ 6. Add `push` to announcement (or other) message types’ allowed/default channels when Planner should select it.
26
+
27
+ ## Not in this release
28
+
29
+ - Host channel policy / Pick’em enablement (host repo)
30
+ - Native client token lifecycle / CT FE `beforeLogout`
31
+ - EAS/APNs credentials or TestFlight proof
@@ -4,6 +4,7 @@ Host-facing upgrade / change summaries for CommandTower releases.
4
4
 
5
5
  | Version | Summary |
6
6
  |---------|---------|
7
+ | [0.14.0](0.14.0.md) | Expo push Messaging channel (`config.messaging.expo`) + `/api/me/push*` registration HTTP; `me_push` RBAC |
7
8
  | [0.13.1](0.13.1.md) | Canonical `Intervention::Severity` constants (`blocking`, `warning`, `informational`) |
8
9
  | [0.13.0](0.13.0.md) | Account self-service deletion (`DELETE /api/me/account`); tombstone + PII scrub; `me_account` RBAC; `users.deleted_at` migration |
9
10
  | [0.12.0](0.12.0.md) | Intervention envelope serializers/deserializers; product-tool admin_scope without Users/Audit narrowing |
@@ -68,6 +68,13 @@ entities:
68
68
  - name: me_pushover_verification
69
69
  controller: CommandTower::Me::Pushover::VerificationsController
70
70
  only: create
71
+ - name: me_push
72
+ controller: CommandTower::Me::PushController
73
+ only:
74
+ - index
75
+ - create
76
+ - update
77
+ - destroy
71
78
  - name: me_audit_events
72
79
  controller: CommandTower::Me::AuditEventsController
73
80
  only:
@@ -3,6 +3,7 @@
3
3
  require "class_composer"
4
4
  require "command_tower/configuration/messaging/sms"
5
5
  require "command_tower/configuration/messaging/pushover"
6
+ require "command_tower/configuration/messaging/expo"
6
7
 
7
8
  module CommandTower
8
9
  module Configuration
@@ -54,6 +55,11 @@ module CommandTower
54
55
  desc: "Pushover verification transport configuration (credential validate + test notification)",
55
56
  allowed: Pushover,
56
57
  default: Pushover.new
58
+
59
+ add_composer :expo,
60
+ desc: "Expo Push delivery configuration (Messaging push channel; optional access_token)",
61
+ allowed: Expo,
62
+ default: Expo.new
57
63
  end
58
64
  end
59
65
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "class_composer"
4
+
5
+ module CommandTower
6
+ module Configuration
7
+ module Messaging
8
+ class Expo < ::CommandTower::Configuration::Base
9
+ include ClassComposer::Generator
10
+
11
+ ADAPTERS = %w[disabled fake log http].freeze
12
+
13
+ add_composer :adapter,
14
+ desc: "Expo Push delivery: disabled | fake | log | http. Default disabled (fail closed).",
15
+ allowed: String,
16
+ default: "disabled",
17
+ validator: ->(val) { ADAPTERS.include?(val.to_s) },
18
+ invalid_message: ->(val) { "Provided #{val.inspect}. Allowed: #{ADAPTERS.join(', ')}" }
19
+
20
+ add_composer :api_base_url,
21
+ desc: "Expo Push API origin (send = {base}/send, receipts = {base}/getReceipts).",
22
+ allowed: String,
23
+ default: "https://exp.host/--/api/v2/push"
24
+
25
+ add_composer :timeout_seconds,
26
+ desc: "HTTP open/read timeout seconds for Expo Push provider calls.",
27
+ allowed: Integer,
28
+ default: 5
29
+
30
+ add_composer :access_token,
31
+ desc: "Optional Expo Push access token. When non-blank, send Authorization: Bearer. " \
32
+ "Required only if the Expo project enables enhanced push security. Never log.",
33
+ allowed: String,
34
+ default: ""
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CommandTower
4
- VERSION = "0.13.1"
4
+ VERSION = "0.14.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: command_tower
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.1
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - matt-taylor
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-30 00:00:00.000000000 Z
11
+ date: 2026-09-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: phonelib
@@ -218,6 +218,7 @@ files:
218
218
  - app/controllers/command_tower/me/phone_verification/verifications_controller.rb
219
219
  - app/controllers/command_tower/me/phone_verification/verify_controller.rb
220
220
  - app/controllers/command_tower/me/preferences_controller.rb
221
+ - app/controllers/command_tower/me/push_controller.rb
221
222
  - app/controllers/command_tower/me/pushover/verifications_controller.rb
222
223
  - app/controllers/command_tower/me/pushover_controller.rb
223
224
  - app/controllers/command_tower/me_controller.rb
@@ -254,6 +255,7 @@ files:
254
255
  - app/deserializers/command_tower/deserializers/me/change_password_deserializer.rb
255
256
  - app/deserializers/command_tower/deserializers/me/delete_account_deserializer.rb
256
257
  - app/deserializers/command_tower/deserializers/me/phone_verification/verify_deserializer.rb
258
+ - app/deserializers/command_tower/deserializers/me/push/token_deserializer.rb
257
259
  - app/deserializers/command_tower/deserializers/me/pushover/credentials_deserializer.rb
258
260
  - app/deserializers/command_tower/deserializers/me/update_name_deserializer.rb
259
261
  - app/deserializers/command_tower/deserializers/me/update_phone_deserializer.rb
@@ -267,6 +269,8 @@ files:
267
269
  - app/errors/command_tower/errors/account/phone_verification_send_failed_error.rb
268
270
  - app/errors/command_tower/errors/account/phone_verification_stale_error.rb
269
271
  - app/errors/command_tower/errors/account/phone_verification_throttled_error.rb
272
+ - app/errors/command_tower/errors/account/push_capability_unavailable_error.rb
273
+ - app/errors/command_tower/errors/account/push_endpoint_not_found_error.rb
270
274
  - app/errors/command_tower/errors/account/pushover_already_configured_error.rb
271
275
  - app/errors/command_tower/errors/account/pushover_capability_unavailable_error.rb
272
276
  - app/errors/command_tower/errors/account/pushover_not_configured_error.rb
@@ -363,6 +367,7 @@ files:
363
367
  - app/serializers/command_tower/serializers/me/account_serializer.rb
364
368
  - app/serializers/command_tower/serializers/me/change_password_response_serializer.rb
365
369
  - app/serializers/command_tower/serializers/me/delete_account_response_serializer.rb
370
+ - app/serializers/command_tower/serializers/me/push_serializer.rb
366
371
  - app/serializers/command_tower/serializers/me/pushover_serializer.rb
367
372
  - app/serializers/command_tower/serializers/messaging/inbox.rb
368
373
  - app/serializers/command_tower/serializers/messaging/preferences/catalog_serializer.rb
@@ -486,6 +491,9 @@ files:
486
491
  - app/services/command_tower/messaging/execution/adapter_result.rb
487
492
  - app/services/command_tower/messaging/execution/adapters/email/adapter.rb
488
493
  - app/services/command_tower/messaging/execution/adapters/email/configuration.rb
494
+ - app/services/command_tower/messaging/execution/adapters/expo/adapter.rb
495
+ - app/services/command_tower/messaging/execution/adapters/expo/configuration.rb
496
+ - app/services/command_tower/messaging/execution/adapters/expo/http_client.rb
489
497
  - app/services/command_tower/messaging/execution/adapters/fake_adapter.rb
490
498
  - app/services/command_tower/messaging/execution/adapters/pushover/adapter.rb
491
499
  - app/services/command_tower/messaging/execution/adapters/pushover/configuration.rb
@@ -579,6 +587,7 @@ files:
579
587
  - app/services/command_tower/messaging/rendering/channel_renderer.rb
580
588
  - app/services/command_tower/messaging/rendering/render_error.rb
581
589
  - app/services/command_tower/messaging/rendering/rendered_payload.rb
590
+ - app/services/command_tower/messaging/rendering/rendered_push_payload.rb
582
591
  - app/services/command_tower/messaging/rendering/rendered_pushover_payload.rb
583
592
  - app/services/command_tower/messaging/rendering/rendered_sms_payload.rb
584
593
  - app/services/command_tower/secrets.rb
@@ -590,6 +599,11 @@ files:
590
599
  - app/services/command_tower/services/account/clear_phone.rb
591
600
  - app/services/command_tower/services/account/phone_verification/send.rb
592
601
  - app/services/command_tower/services/account/phone_verification/verify.rb
602
+ - app/services/command_tower/services/account/push/create.rb
603
+ - app/services/command_tower/services/account/push/ct_support.rb
604
+ - app/services/command_tower/services/account/push/destroy.rb
605
+ - app/services/command_tower/services/account/push/list.rb
606
+ - app/services/command_tower/services/account/push/replace.rb
593
607
  - app/services/command_tower/services/account/pushover/create.rb
594
608
  - app/services/command_tower/services/account/pushover/ct_support.rb
595
609
  - app/services/command_tower/services/account/pushover/destroy.rb
@@ -642,6 +656,7 @@ files:
642
656
  - app/services/command_tower/services/me/capabilities.rb
643
657
  - app/services/command_tower/services/me/change_password.rb
644
658
  - app/services/command_tower/services/me/delete_account.rb
659
+ - app/services/command_tower/services/me/push_product_gate.rb
645
660
  - app/services/command_tower/services/me/pushover_product_gate.rb
646
661
  - app/services/command_tower/services/me/sms_product_gate.rb
647
662
  - app/services/command_tower/services/me/update_name.rb
@@ -660,6 +675,7 @@ files:
660
675
  - app/views/command_tower/email_verification_mailer/verify_email.html.erb
661
676
  - app/views/command_tower/messaging/rendering/email.html.erb
662
677
  - app/views/command_tower/messaging/rendering/email.text.erb
678
+ - app/views/command_tower/messaging/rendering/push.text.erb
663
679
  - app/views/command_tower/messaging/rendering/pushover.text.erb
664
680
  - app/views/command_tower/messaging/rendering/sms.text.erb
665
681
  - app/views/command_tower/password_reset_mailer/reset_password.html.erb
@@ -720,6 +736,11 @@ files:
720
736
  - app/workflows/command_tower/workflows/me/error_status.rb
721
737
  - app/workflows/command_tower/workflows/me/phone_verification/send_workflow.rb
722
738
  - app/workflows/command_tower/workflows/me/phone_verification/verify_workflow.rb
739
+ - app/workflows/command_tower/workflows/me/push/create_workflow.rb
740
+ - app/workflows/command_tower/workflows/me/push/destroy_workflow.rb
741
+ - app/workflows/command_tower/workflows/me/push/index_workflow.rb
742
+ - app/workflows/command_tower/workflows/me/push/replace_workflow.rb
743
+ - app/workflows/command_tower/workflows/me/push/workflow_support.rb
723
744
  - app/workflows/command_tower/workflows/me/pushover/create_workflow.rb
724
745
  - app/workflows/command_tower/workflows/me/pushover/destroy_workflow.rb
725
746
  - app/workflows/command_tower/workflows/me/pushover/replace_workflow.rb
@@ -776,6 +797,7 @@ files:
776
797
  - docs/upgrades/0.12.0.md
777
798
  - docs/upgrades/0.13.0.md
778
799
  - docs/upgrades/0.13.1.md
800
+ - docs/upgrades/0.14.0.md
779
801
  - docs/upgrades/README.md
780
802
  - lib/command_tower.rb
781
803
  - lib/command_tower/admin_scope.rb
@@ -822,6 +844,7 @@ files:
822
844
  - lib/command_tower/configuration/login/strategy/plain_text/lockable.rb
823
845
  - lib/command_tower/configuration/login/strategy/plain_text/password_reset.rb
824
846
  - lib/command_tower/configuration/messaging/config.rb
847
+ - lib/command_tower/configuration/messaging/expo.rb
825
848
  - lib/command_tower/configuration/messaging/pushover.rb
826
849
  - lib/command_tower/configuration/messaging/sms.rb
827
850
  - lib/command_tower/configuration/otp/config.rb