command_tower 0.16.0 → 0.18.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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -8
  3. data/app/controllers/command_tower/application_controller.rb +1 -0
  4. data/app/controllers/command_tower/me/inbox_controller.rb +16 -0
  5. data/app/controllers/concerns/command_tower/execution/client_compatibility_boundary.rb +39 -0
  6. data/app/deserializers/command_tower/deserializers/messaging/inbox.rb +85 -0
  7. data/app/errors/command_tower/errors/client_update_required_error.rb +27 -0
  8. data/app/jobs/command_tower/messaging/communications/produce_recipient_job.rb +2 -0
  9. data/app/models/command_tower/messaging/communication.rb +31 -0
  10. data/app/serializers/command_tower/serializers/messaging/inbox.rb +45 -1
  11. data/app/services/command_tower/email_theme/resolver.rb +45 -0
  12. data/app/services/command_tower/messaging/accept/coordinator.rb +16 -1
  13. data/app/services/command_tower/messaging/accept/persister.rb +13 -0
  14. data/app/services/command_tower/messaging/contract/mappers/communication_mapper.rb +7 -0
  15. data/app/services/command_tower/messaging/contract/results/communication_result.rb +1 -0
  16. data/app/services/command_tower/messaging/inbox/conversation_result.rb +31 -0
  17. data/app/services/command_tower/messaging/inbox/entry_result.rb +36 -0
  18. data/app/services/command_tower/messaging/inbox/reader.rb +231 -18
  19. data/app/services/command_tower/messaging/inbox.rb +4 -0
  20. data/app/services/command_tower/messaging/notification_types/declaration.rb +14 -1
  21. data/app/services/command_tower/messaging/rendering/channel_renderer.rb +44 -13
  22. data/app/services/command_tower/messaging/rendering/inbox_document.rb +131 -0
  23. data/app/services/command_tower/messaging/rendering/inbox_document_renderer.rb +224 -0
  24. data/app/services/command_tower/messaging/rendering/inbox_presentation_resolver.rb +52 -0
  25. data/app/services/command_tower/messaging/rendering/inbox_presentation_snapshot.rb +64 -0
  26. data/app/services/command_tower/messaging/rendering/template_resolver.rb +128 -0
  27. data/app/services/command_tower/messaging.rb +5 -1
  28. data/app/services/command_tower/services/client_compatibility/evaluate.rb +219 -0
  29. data/app/services/command_tower/services/messaging/communications/produce.rb +4 -0
  30. data/app/services/command_tower/services/messaging/communications/produce_many.rb +6 -0
  31. data/app/services/command_tower/services/messaging/inbox.rb +55 -2
  32. data/app/views/command_tower/email_verification_mailer/verify_email.html.erb +18 -17
  33. data/app/views/command_tower/messaging/rendering/email.html.erb +6 -5
  34. data/app/views/command_tower/password_reset_mailer/reset_password.html.erb +24 -23
  35. data/app/workflows/command_tower/workflows/auth/plain_text/login_workflow.rb +3 -0
  36. data/app/workflows/command_tower/workflows/auth/session/show_workflow.rb +3 -0
  37. data/app/workflows/command_tower/workflows/client_compatibility/evaluate_workflow.rb +83 -0
  38. data/app/workflows/command_tower/workflows/client_compatibility/recommendation_meta.rb +25 -0
  39. data/app/workflows/command_tower/workflows/messaging/communications/produce_recipient_workflow.rb +5 -1
  40. data/app/workflows/command_tower/workflows/messaging/inbox.rb +15 -1
  41. data/config/routes.rb +1 -0
  42. data/db/migrate/20261010000001_add_inbox_presentation_snapshot_to_messaging_communications.rb +7 -0
  43. data/db/migrate/20261010000002_add_conversation_identity_to_messaging_communications.rb +10 -0
  44. data/docs/api_reference.md +13 -2
  45. data/docs/authorization.md +1 -1
  46. data/docs/bootstrap/00-ownership.md +77 -0
  47. data/docs/bootstrap/01-docker-make-compose.md +267 -0
  48. data/docs/bootstrap/02-create-the-rails-app.md +75 -0
  49. data/docs/bootstrap/03-pin-the-gem.md +53 -0
  50. data/docs/bootstrap/04-secrets-and-env.md +59 -0
  51. data/docs/bootstrap/05-install-migrate-doctor.md +74 -0
  52. data/docs/bootstrap/06-mount-and-health.md +53 -0
  53. data/docs/bootstrap/07-execution-bases.md +38 -0
  54. data/docs/bootstrap/08-initializer.md +90 -0
  55. data/docs/bootstrap/09-rbac.md +83 -0
  56. data/docs/bootstrap/10-roles-and-gates.md +62 -0
  57. data/docs/bootstrap/11-auth-client-path.md +57 -0
  58. data/docs/bootstrap/12-smoke-check.md +81 -0
  59. data/docs/bootstrap/13-optional.md +50 -0
  60. data/docs/bootstrap/14-sanity-checks.md +44 -0
  61. data/docs/bootstrap/README.md +90 -0
  62. data/docs/cookie_authentication_guide.md +11 -0
  63. data/docs/extending.md +5 -4
  64. data/docs/host_integration_guide.md +3 -246
  65. data/docs/initializing.md +37 -28
  66. data/docs/messaging_integration_guide.md +39 -1
  67. data/docs/principal_capabilities.md +1 -1
  68. data/docs/upgrades/0.10.0.md +5 -5
  69. data/docs/upgrades/0.11.0.md +5 -5
  70. data/docs/upgrades/0.17.0.md +33 -0
  71. data/docs/upgrades/0.18.0.md +35 -0
  72. data/docs/upgrades/README.md +3 -1
  73. data/lib/command_tower/authorization/default.yml +1 -0
  74. data/lib/command_tower/client_compatibility/version.rb +45 -0
  75. data/lib/command_tower/client_compatibility.rb +23 -0
  76. data/lib/command_tower/configuration/config.rb +6 -0
  77. data/lib/command_tower/configuration/email_theme/config.rb +69 -0
  78. data/lib/command_tower/configuration/registry/client_compatibility/binding_definition.rb +42 -0
  79. data/lib/command_tower/configuration/registry/client_compatibility/config.rb +353 -0
  80. data/lib/command_tower/configuration/registry/client_compatibility/contract_definition.rb +16 -0
  81. data/lib/command_tower/configuration/registry/client_compatibility/entity_requirement_definition.rb +67 -0
  82. data/lib/command_tower/configuration/registry/client_compatibility/minimum_overrides.rb +46 -0
  83. data/lib/command_tower/configuration/registry/client_compatibility/platform_definition.rb +66 -0
  84. data/lib/command_tower/configuration/registry/config.rb +14 -0
  85. data/lib/command_tower/configuration/registry/inbox_presentations/config.rb +105 -0
  86. data/lib/command_tower/configuration/registry/inbox_presentations/presentation_definition.rb +71 -0
  87. data/lib/command_tower/current.rb +1 -0
  88. data/lib/command_tower/engine.rb +4 -0
  89. data/lib/command_tower/inbox_presentations.rb +19 -0
  90. data/lib/command_tower/install/baseline.rb +2 -0
  91. data/lib/command_tower/version.rb +1 -1
  92. metadata +47 -2
@@ -0,0 +1,53 @@
1
+ # 03 — Pin the CommandTower gem
2
+
3
+ ## Purpose
4
+
5
+ Depend on released CommandTower from **RubyGems** (or a git tag). Path pins are maintainer Local mode, not the committed default for a new host.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`02-create-the-rails-app.md`](02-create-the-rails-app.md): `make bundle` works
10
+ - You know the CommandTower version (or git tag) this host should adopt
11
+
12
+ ## Files the host edits
13
+
14
+ `Gemfile` — add:
15
+
16
+ ```ruby
17
+ gem "command_tower"
18
+ ```
19
+
20
+ Pin when you need a floor:
21
+
22
+ ```ruby
23
+ gem "command_tower", "~> X.Y"
24
+ ```
25
+
26
+ Git tag (only if your org does not publish the gem to the RubyGems source the host uses):
27
+
28
+ ```ruby
29
+ gem "command_tower", git: "https://github.com/<org>/command_tower.git", tag: "vX.Y.Z"
30
+ ```
31
+
32
+ **Path pins** (`path: "../command_tower"`) are for engine maintainers iterating locally. They are not the default a new product host commits. If you use a path pin in a workspace, do not document it as the production pin.
33
+
34
+ ## Procedure
35
+
36
+ 1. Add the gem line.
37
+ 2. Refresh lockfile **in the container**:
38
+
39
+ ```bash
40
+ make bundle
41
+ ```
42
+
43
+ 3. Rebuild if the image copies `Gemfile` / `Gemfile.lock` at build time:
44
+
45
+ ```bash
46
+ make build
47
+ ```
48
+
49
+ Do not run `bundle install` on the Mac.
50
+
51
+ ## Stop
52
+
53
+ `Gemfile.lock` lists `command_tower`. `make bundle` succeeds. Next: [`04-secrets-and-env.md`](04-secrets-and-env.md).
@@ -0,0 +1,59 @@
1
+ # 04 — Secrets and env
2
+
3
+ ## Purpose
4
+
5
+ Give Compose the secrets CommandTower **doctor** requires. Without them, install may succeed and doctor will fail.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`03-pin-the-gem.md`](03-pin-the-gem.md)
10
+ - Compose file from [`01-docker-make-compose.md`](01-docker-make-compose.md)
11
+
12
+ ## Files the host creates / edits
13
+
14
+ | File | Role |
15
+ |------|------|
16
+ | `.env` (gitignored) | Local values interpolated by Compose |
17
+ | `.env.example` (committed, no secrets) | Names only |
18
+ | `docker-compose.yml` `environment:` | Already stubbed in step 01 |
19
+
20
+ Required (names match [`../initializing.md`](../initializing.md)):
21
+
22
+ | Secret | Typical env | Used for |
23
+ |--------|-------------|----------|
24
+ | JWT HMAC | `SECRET_KEY_BASE` (or `config.jwt.hmac_secret`) | Access tokens |
25
+ | Signup-session JWT | `SIGNUP_SESSION_JWT_SECRET` | Email verification / signup session |
26
+ | Password-recovery JWT | `PASSWORD_RECOVERY_SESSION_JWT_SECRET` | Password reset session |
27
+
28
+ Doctor checks these. Failures abort with remediation text.
29
+
30
+ `.env.example`:
31
+
32
+ ```
33
+ SECRET_KEY_BASE=
34
+ SIGNUP_SESSION_JWT_SECRET=
35
+ PASSWORD_RECOVERY_SESSION_JWT_SECRET=
36
+ ```
37
+
38
+ Generate long random values for `.env`. Do not commit `.env`. Do not reuse production secrets in development.
39
+
40
+ The initializer generated in step 05 can read these via `ENV.fetch`. You do not need a complete initializer yet.
41
+
42
+ Optional later: SMTP, SMS, Pushover credentials — [`13-optional.md`](13-optional.md) and [`../messaging_integration_guide.md`](../messaging_integration_guide.md). Not required for `GET /me`.
43
+
44
+ This step is **local** env for Compose. Production secret distribution is out of scope ([README](README.md#this-is-not-deployment)).
45
+
46
+ ## Procedure
47
+
48
+ 1. Create `.env` with three distinct secrets.
49
+ 2. Confirm `docker-compose.yml` passes them into `api`.
50
+ 3. Recreate the api container if it was already running:
51
+
52
+ ```bash
53
+ make halt
54
+ make build
55
+ ```
56
+
57
+ ## Stop
58
+
59
+ Compose interpolates the three secrets into `api`. Next: [`05-install-migrate-doctor.md`](05-install-migrate-doctor.md).
@@ -0,0 +1,74 @@
1
+ # 05 — Install, migrate, doctor
2
+
3
+ ## Purpose
4
+
5
+ Copy engine migrations, generate the initializer (and default mount unless skipped), migrate, and run doctor. **Invocation is Make.** The contract for flags and schema is [`../initializing.md`](../initializing.md) — do not treat this page as a second copy of that contract.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`04-secrets-and-env.md`](04-secrets-and-env.md)
10
+ - Database service healthy (`make` recipes that run Rails will start `depends_on`)
11
+
12
+ ## Files this step creates
13
+
14
+ | File | Role |
15
+ |------|------|
16
+ | `db/migrate/*command_tower.rb` | Installed copies of engine migrations (do not hand-edit) |
17
+ | `config/initializers/command_tower.rb` | Generated unless `SKIP_CONFIGURE=1` |
18
+ | Engine mount in `config/routes.rb` | Unless `SKIP_MOUNT=1` or `--skip-routes` |
19
+
20
+ `command_tower:install` does **not** run `db:migrate`. Installing and migrating stay separate on purpose.
21
+
22
+ ## Procedure
23
+
24
+ If the host will own `GET /api/healthz` (or any route that must be declared **before** the engine mount), skip the generator mount and add routes in [`06-mount-and-health.md`](06-mount-and-health.md):
25
+
26
+ ```bash
27
+ make rails ARGS='command_tower:install' SKIP_MOUNT=1
28
+ ```
29
+
30
+ Otherwise:
31
+
32
+ ```bash
33
+ make rails ARGS='command_tower:install'
34
+ ```
35
+
36
+ Then:
37
+
38
+ ```bash
39
+ make db-prepare
40
+ make migrate
41
+ make doctor
42
+ ```
43
+
44
+ `db-prepare` creates the development database if needed. `migrate` applies host and CommandTower migrations.
45
+
46
+ Existing customized hosts (initializer and mount already present):
47
+
48
+ ```bash
49
+ make rails ARGS='command_tower:install' SKIP_CONFIGURE=1
50
+ make migrate
51
+ make doctor
52
+ ```
53
+
54
+ `FORCE=1` overwrites `config/initializers/command_tower.rb` — do not use it after you have customized the initializer unless you intend to regenerate.
55
+
56
+ The **task name** inside the container is `bin/rails command_tower:install`. Operators never type that on the Mac.
57
+
58
+ ### What install does (summary)
59
+
60
+ 1. `command_tower:install:migrations` — copies engine migrations with `*.command_tower.rb` scope.
61
+ 2. `rails g command_tower:configure` unless skipped — initializer + mount.
62
+ 3. Prints next steps.
63
+
64
+ Full flag table: [`../initializing.md`](../initializing.md).
65
+
66
+ Doctor checks Rails compatibility, engine baseline migrations, host-installed copies, JWT / session secrets, and messaging adapter names. It does **not** probe Redis/SMTP connectivity.
67
+
68
+ ## In-engine dummy
69
+
70
+ Generated initializer shape: [`../../rails_app/config/initializers/command_tower.rb`](../../rails_app/config/initializers/command_tower.rb). That file is a dummy-host example, not a product dump.
71
+
72
+ ## Stop
73
+
74
+ Install is idempotent if re-run. Migrations applied. `make doctor` exits 0 (or you have a documented doctor failure you will fix in steps 08–10). Next: [`06-mount-and-health.md`](06-mount-and-health.md).
@@ -0,0 +1,53 @@
1
+ # 06 — Mount and health
2
+
3
+ ## Purpose
4
+
5
+ Mount CommandTower where the product needs it, and keep a host health endpoint **outside** the engine when you skipped the generator mount.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`05-install-migrate-doctor.md`](05-install-migrate-doctor.md)
10
+
11
+ ## Files the host edits
12
+
13
+ `config/routes.rb`
14
+
15
+ ## Procedure
16
+
17
+ ### Default (generator mounted)
18
+
19
+ `command_tower:install` without `SKIP_MOUNT` typically mounts:
20
+
21
+ ```ruby
22
+ mount CommandTower::Engine => "/api"
23
+ ```
24
+
25
+ or at `/`, depending on generator defaults. Confirm the file. Engine routes then live under that prefix (`GET /api/me` vs `GET /me`).
26
+
27
+ Use **one** mount. Do not mount the engine twice.
28
+
29
+ ### Host-owned healthz (`SKIP_MOUNT=1`)
30
+
31
+ Declare host routes **before** the engine so health checks never depend on engine routing:
32
+
33
+ ```ruby
34
+ Rails.application.routes.draw do
35
+ get "/api/healthz", to: "health#show"
36
+
37
+ mount CommandTower::Engine => "/api"
38
+ end
39
+ ```
40
+
41
+ `HealthController#show` is host-owned (liveness). Keep it trivial.
42
+
43
+ If the engine is mounted at `/`, a host `/healthz` still belongs **before** the mount.
44
+
45
+ ### Accidental public mount
46
+
47
+ If you mounted at `/` and needed `/api`, or the generator mounted when you needed to skip: fix `config/routes.rb` by hand. Re-running configure with `FORCE=1` is not the tool for route cleanup.
48
+
49
+ More on mount mistakes: [`../initializing.md`](../initializing.md) troubleshooting.
50
+
51
+ ## Stop
52
+
53
+ `config/routes.rb` has exactly one engine mount at the prefix your API and frontend will call. Host `healthz` (if any) is declared before that mount. Next: [`07-execution-bases.md`](07-execution-bases.md).
@@ -0,0 +1,38 @@
1
+ # 07 — Execution-boundary bases
2
+
3
+ ## Purpose
4
+
5
+ Host controllers and jobs inherit CommandTower bases so authentication, request context, envelope rendering, and job execution stay on the platform path.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`06-mount-and-health.md`](06-mount-and-health.md)
10
+
11
+ ## Files the host edits
12
+
13
+ | File | Inherit |
14
+ |------|---------|
15
+ | `app/controllers/application_controller.rb` | `CommandTower::ApplicationController` |
16
+ | `app/jobs/application_job.rb` | `CommandTower::ApplicationJob` |
17
+
18
+ ## Procedure
19
+
20
+ Replace the default `ActionController::API` / `ActiveJob::Base` superclasses:
21
+
22
+ ```ruby
23
+ class ApplicationController < CommandTower::ApplicationController
24
+ end
25
+ ```
26
+
27
+ ```ruby
28
+ class ApplicationJob < CommandTower::ApplicationJob
29
+ end
30
+ ```
31
+
32
+ Host controllers then subclass `ApplicationController` as usual. Do not subclass `ActionController::API` for product JSON that should share CommandTower auth and envelope behavior.
33
+
34
+ Background work invokes **workflows** (via CommandTower job helpers), not services as a full business action. See [`../architecture.md`](../architecture.md).
35
+
36
+ ## Stop
37
+
38
+ `ApplicationController` and `ApplicationJob` inherit CommandTower bases. Next: [`08-initializer.md`](08-initializer.md).
@@ -0,0 +1,90 @@
1
+ # 08 — Initializer
2
+
3
+ ## Purpose
4
+
5
+ Set **host_key**, secrets, local mail, and (when the client is Command Tower frontend on web) cookies. The generated file documents every class_composer option; do not uncomment the world.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`07-execution-bases.md`](07-execution-bases.md)
10
+ - Secrets from [`04-secrets-and-env.md`](04-secrets-and-env.md) already in Compose
11
+
12
+ ## Files the host edits
13
+
14
+ `config/initializers/command_tower.rb` (created by install)
15
+
16
+ In-engine dummy for **shape**: [`../../rails_app/config/initializers/command_tower.rb`](../../rails_app/config/initializers/command_tower.rb). Copy **intent** (host_key, secrets, later RBAC/gates), not the dummy’s engine-dev values as a product identity.
17
+
18
+ ## Procedure
19
+
20
+ ### `host_key` (required)
21
+
22
+ Server-bound host/product identity for CT-generic user-scoped state (experience states). **Not** client-supplied.
23
+
24
+ ```ruby
25
+ CommandTower.configure do |config|
26
+ config.application.host_key = "your_product"
27
+ end
28
+ ```
29
+
30
+ Blank `host_key` → Me experience-state routes return **503**.
31
+
32
+ The dummy sets `config.application.host_key = "command_tower"`. A product host uses its **own** key.
33
+
34
+ ### Secrets
35
+
36
+ Prefer ENV already injected by Compose:
37
+
38
+ ```ruby
39
+ config.jwt.hmac_secret = ENV.fetch("SECRET_KEY_BASE")
40
+ config.signup_session.jwt_secret = ENV.fetch("SIGNUP_SESSION_JWT_SECRET")
41
+ config.password_recovery_session.jwt_secret = ENV.fetch("PASSWORD_RECOVERY_SESSION_JWT_SECRET")
42
+ ```
43
+
44
+ Exact setter names are in the generated initializer comments and [`../initializing.md`](../initializing.md). Match those names; do not invent parallel keys.
45
+
46
+ ### Cookies / CSRF (required for Command Tower frontend on web)
47
+
48
+ Command Tower frontend `defaultSessionStrategy()` is **cookie + CSRF on web** and Bearer on native. If this API will serve that web client, enable cookies **now**. Curl-only smoke in [`12-smoke-check.md`](12-smoke-check.md) can still send `Authorization: Bearer`.
49
+
50
+ Contract and defaults: [`../cookie_authentication_guide.md`](../cookie_authentication_guide.md). Local HTTP Compose:
51
+
52
+ ```ruby
53
+ config.jwt.cookie.enabled = true
54
+ config.jwt.cookie.secure = false
55
+ config.jwt.cookie.same_site = :lax
56
+ config.jwt.cookie.csrf.enabled = true
57
+ ```
58
+
59
+ Do not set `secure = true` on `http://localhost`. Leave `domain` nil (host-only). Defaults are `ct_jwt` and `ct_csrf`. Full field table stays in the cookie guide.
60
+
61
+ A browser stores those cookies by hostname. The port is not part of the key, so two web apps on `localhost` share one `ct_jwt` and one `ct_csrf`. Logout expires both cookies. A second product on the same hostname sets both names, and that host’s frontend `session.csrfCookieName` matches the CSRF name. The header stays `X-CSRF-Token`. A host that runs alone leaves the defaults.
62
+
63
+ ```ruby
64
+ config.jwt.cookie.name = "other_jwt"
65
+ config.jwt.cookie.csrf.cookie_name = "other_csrf"
66
+ ```
67
+
68
+ If the only client is Bearer (`curl`, native-only), leave cookies disabled.
69
+
70
+ ### Email / SMTP
71
+
72
+ Step 02 already set `:file` delivery in `development.rb`. In the initializer, set the public app URL used in mail links (frontend origin, not the API port):
73
+
74
+ ```ruby
75
+ config.application.url = "http://localhost:8081"
76
+ ```
77
+
78
+ `8081` is one product’s Expo port. Use the port this host’s web app actually publishes, and put that same origin in `CORS_ALLOWED_ORIGINS` (step 01). Production SMTP is out of this manual’s deployment scope.
79
+
80
+ ### What not to do
81
+
82
+ - Do not duplicate CommandTower default hashes “for completeness”
83
+ - Do not set `FORCE=1` and regenerate after customizing
84
+ - Do not fork engine internals because a comment looked optional
85
+
86
+ RBAC default role and feature gates are steps 09–10.
87
+
88
+ ## Stop
89
+
90
+ `host_key` is a real product string. JWT and session secrets resolve from ENV. Cookie + CSRF are on if a CT frontend web client will call this API. Next: [`09-rbac.md`](09-rbac.md).
@@ -0,0 +1,83 @@
1
+ # 09 — RBAC
2
+
3
+ ## Purpose
4
+
5
+ Give authenticated users a **product role** that **grants CommandTower entity names**. Without this, login can succeed and `GET /me` returns **403**.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`08-initializer.md`](08-initializer.md)
10
+
11
+ ## Files the host creates
12
+
13
+ `config/rbac_groups.yml` — host-owned composition.
14
+
15
+ CommandTower ships **entity definitions** in `lib/command_tower/authorization/default.yml` (inside the gem). Hosts **grant those names**. Hosts may add host-owned entities for host controllers.
16
+
17
+ In-engine dummy for **shape**: [`../../rails_app/config/rbac_groups.yml`](../../rails_app/config/rbac_groups.yml). Use it to see a `member` grant list and that Admin bundles are **host policy**. Do not copy dummy-only entities (`dummy_admin_example`) into a product host.
18
+
19
+ Contract: [`../authorization.md`](../authorization.md), [`../authentication_authorization_guide.md`](../authentication_authorization_guide.md).
20
+
21
+ ## Procedure
22
+
23
+ ### 1. Author `member` (minimum for Me)
24
+
25
+ Grant the Me/Auth entity names your smoke path needs. A typical starting `member` (from the dummy, minus product-specific extras you do not want yet):
26
+
27
+ ```yaml
28
+ groups:
29
+ member:
30
+ description: Standard authenticated application user
31
+ entities:
32
+ - email_verification_send
33
+ - email_verification_verify
34
+ - session
35
+ - me
36
+ - profile
37
+ - me_name
38
+ - me_password
39
+ - me_account
40
+ - me_inbox
41
+ - me_audit_events
42
+ - me_preferences
43
+ - me_phone
44
+ - me_phone_verification_send
45
+ - me_phone_verification_verify
46
+ - me_pushover
47
+ - me_pushover_verification
48
+ - me_push
49
+ - me_experience_states
50
+ - principal_capabilities
51
+ ```
52
+
53
+ Trim unused Me surfaces if you want a thinner first grant (you **must** keep `me` and `session` for the smoke in [`12-smoke-check.md`](12-smoke-check.md)). Missing `me` → **403** on `GET /me`.
54
+
55
+ ### 2. `default_membership_role`
56
+
57
+ In `config/initializers/command_tower.rb`:
58
+
59
+ ```ruby
60
+ config.authorization.default_membership_role = "member"
61
+ ```
62
+
63
+ New users then receive `member` without a manual assign. If you omit this, you must assign roles some other supported way before Me works.
64
+
65
+ ### 3. Admin roles (not required to finish bootstrap)
66
+
67
+ Do **not** auto-grant new Admin capabilities to a generic Admin role. CommandTower does not ship an accumulating operational `admin` role. `owner` (`entities: true`) is full-access and distinct from host Admin bundles.
68
+
69
+ If you add Admin later, copy the **pattern** from the dummy (`audit_operator`, `operations_admin`, …): explicit entity lists on a host role. See [`../admin_workspace.md`](../admin_workspace.md).
70
+
71
+ The frontend workspace and user detail call these entities. Grant each one the role should have:
72
+
73
+ - `admin_workspace`
74
+ - `admin_users`
75
+ - `admin_audit_events`
76
+ - `admin_messaging_announcements`
77
+ - `admin_impersonation`
78
+
79
+ `admin_impersonation` is what shows Impersonate. The frontend does not add a route for it.
80
+
81
+ ## Stop
82
+
83
+ `config/rbac_groups.yml` exists with a `member` group that grants `me` (and the rest you need). `default_membership_role` is `"member"` (or you have a documented assign path). Next: [`10-roles-and-gates.md`](10-roles-and-gates.md).
@@ -0,0 +1,62 @@
1
+ # 10 — Roles and feature gates
2
+
3
+ ## Purpose
4
+
5
+ Assign the default membership role (if you did not in step 09) and **enable** the HTTP gates the smoke path needs. Gates default **off** → **404**.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`09-rbac.md`](09-rbac.md)
10
+
11
+ ## Files the host edits
12
+
13
+ `config/initializers/command_tower.rb`
14
+
15
+ Opening a `with_*` block typically sets its enable flag. Still set `enable` / `enabled` explicitly so a later comment-uncomment does not leave you on the default **off** path.
16
+
17
+ ## Procedure
18
+
19
+ ### Default role
20
+
21
+ ```ruby
22
+ config.authorization.default_membership_role = "member"
23
+ ```
24
+
25
+ ### Login / verify / reset (minimum for Me + CT frontend guest screens)
26
+
27
+ ```ruby
28
+ config.login.with_plain_text do |plain_text_config|
29
+ plain_text_config.enable = true
30
+
31
+ plain_text_config.with_email_verify do |email_verify_config|
32
+ email_verify_config.enable = true
33
+ # Curl smoke (step 12) can login before the user verifies.
34
+ # 0.minutes → GET /me is 412 until email is verified.
35
+ email_verify_config.verify_email_required_within = 7.days
36
+ end
37
+
38
+ plain_text_config.with_password_reset do |password_reset_config|
39
+ password_reset_config.enabled = true
40
+ end
41
+ end
42
+ ```
43
+
44
+ ### Availability (CT frontend signup)
45
+
46
+ If the guest signup screen checks username/email availability, enable those gates or the client sees **404**:
47
+
48
+ ```ruby
49
+ config.username.with_realtime_username_check do |realtime_username_check_config|
50
+ realtime_username_check_config.enable = true
51
+ end
52
+
53
+ config.signup_session.email_availability.enable = true
54
+ ```
55
+
56
+ Leave Admin and messaging gates off until you need those routes.
57
+
58
+ Do not use role-**name** checks in host code for authorization. Use entity grants / `allow_everything`. See [`../authorization.md`](../authorization.md).
59
+
60
+ ## Stop
61
+
62
+ Plain-text login is **on**. Register → login → `GET /me` is not **404**. Next: [`11-auth-client-path.md`](11-auth-client-path.md).
@@ -0,0 +1,57 @@
1
+ # 11 — Auth client path
2
+
3
+ ## Purpose
4
+
5
+ Pick **one** client authentication mode and configure the host to match. Mixing a cookie+CSRF frontend with cookies disabled will fail in the browser. `curl` can still send Bearer on the same host.
6
+
7
+ ## Prerequisites
8
+
9
+ - [`10-roles-and-gates.md`](10-roles-and-gates.md)
10
+ - `gem "rack-cors"` from [`02-create-the-rails-app.md`](02-create-the-rails-app.md) if a browser will call this API
11
+
12
+ ## Two supported modes
13
+
14
+ | Mode | Client sends | Host initializer |
15
+ |------|----------------|------------------|
16
+ | Bearer | `Authorization: Bearer <jwt>` | Cookie JWT disabled |
17
+ | Cookie + CSRF | HttpOnly JWT cookie + CSRF header/cookie | Cookie + CSRF **enabled** (step 08) |
18
+
19
+ Command Tower **frontend** on web uses `defaultSessionStrategy()` → cookie + CSRF. Native uses Bearer. Enable cookies when that web client exists; keep Bearer working for `curl` and native.
20
+
21
+ Contract: [`../cookie_authentication_guide.md`](../cookie_authentication_guide.md).
22
+
23
+ Do not invent a third session store. Do not put JWTs in `localStorage` as a “simpler cookie.”
24
+
25
+ ## Procedure
26
+
27
+ 1. If the client is Command Tower frontend on web: cookies + CSRF already on from step 08 (`secure = false` on HTTP localhost).
28
+ 2. If Bearer-only: leave cookies disabled; step 12 uses `Authorization`.
29
+ 3. If a **browser on another origin** calls the API, add host CORS. CommandTower does not ship CORS.
30
+
31
+ `config/initializers/cors.rb`:
32
+
33
+ ```ruby
34
+ allowed_origins = ENV.fetch(
35
+ "CORS_ALLOWED_ORIGINS",
36
+ "http://localhost:8081,http://localhost:8082,http://localhost:19006"
37
+ ).split(",").map(&:strip).reject(&:empty?)
38
+
39
+ Rails.application.config.middleware.insert_before 0, Rack::Cors do
40
+ allow do
41
+ origins(*allowed_origins)
42
+ resource "*",
43
+ headers: :any,
44
+ methods: %i[get post put patch delete options head],
45
+ credentials: true,
46
+ expose: ["X-Authorization-Expire", "X-Authorization-Reset"]
47
+ end
48
+ end
49
+ ```
50
+
51
+ `credentials: true` requires an **explicit origin list**, not `*`. Compose already passes `CORS_ALLOWED_ORIGINS` from step 01. The sample origins `8081`, `8082`, and `19006` belong to one web product. Add the origin this Expo web app actually uses, and keep it aligned with `config.application.url` and the API’s published port.
52
+
53
+ Attaching a Command Tower frontend: [Local FE↔BE join](../../../artifacts/COMMANDTOWER_LOCAL_JOIN.md).
54
+
55
+ ## Stop
56
+
57
+ You can state which mode the smoke will use. CORS exists if a browser will call this API. Next: [`12-smoke-check.md`](12-smoke-check.md).
@@ -0,0 +1,81 @@
1
+ # 12 — Smoke check (`GET /me`)
2
+
3
+ ## Purpose
4
+
5
+ Prove the host is a usable Me/Auth API: register, log in, call `GET /me`, receive **200**.
6
+
7
+ ## Prerequisites
8
+
9
+ - Steps 00–11
10
+ - API listening: `make server` (or `make s`) — Puma bound on `0.0.0.0` (step 01)
11
+
12
+ Prefix every path with your engine mount (`/api` if you mounted there). Examples below assume mount at `/api` and host port **3000**.
13
+
14
+ HTTP catalog (full contract): [`../api_reference.md`](../api_reference.md). This page is the **happy path only**.
15
+
16
+ ## Procedure
17
+
18
+ Run `curl` **on the Mac** against the published Compose port. That is HTTP, not Ruby on the host.
19
+
20
+ Login succeeds with **201** (not 200). Register succeeds with **201** and **no** token.
21
+
22
+ ### 1. Register
23
+
24
+ ```bash
25
+ curl -sS -X POST http://localhost:3000/api/auth/register \
26
+ -H "Content-Type: application/json" \
27
+ -d '{
28
+ "first_name": "Ada",
29
+ "last_name": "Lovelace",
30
+ "username": "ada1",
31
+ "email": "ada@example.com",
32
+ "password": "password12",
33
+ "password_confirmation": "password12"
34
+ }'
35
+ ```
36
+
37
+ Expect **201**. No JWT. If email verification is required immediately (`verify_email_required_within = 0.minutes`), either verify using the code in `tmp/mails` or raise the grace period (step 10).
38
+
39
+ If you already have a user in the database, skip to login.
40
+
41
+ ### 2. Login
42
+
43
+ ```bash
44
+ curl -sS -X POST http://localhost:3000/api/auth/plain-text/login \
45
+ -H "Content-Type: application/json" \
46
+ -d '{"identifier": "ada@example.com", "password": "password12"}'
47
+ ```
48
+
49
+ Expect **201** and `data.token`. Cookie mode also sets `Set-Cookie` for `ct_jwt` (and `ct_csrf` when CSRF is on).
50
+
51
+ ### 3. Me
52
+
53
+ ```bash
54
+ TOKEN='<paste data.token>'
55
+ curl -sS -H "Authorization: Bearer ${TOKEN}" http://localhost:3000/api/me
56
+ ```
57
+
58
+ Expect **200** and a Me payload.
59
+
60
+ Cookie mode: omit `Authorization`, send `-b cookies.txt` after login with `-c cookies.txt`, and on mutating requests add `-H "X-CSRF-Token: …"` from the CSRF cookie. `GET /me` still needs the JWT cookie.
61
+
62
+ ## How to read failures
63
+
64
+ | Status | Meaning |
65
+ |--------|---------|
66
+ | **401** | Not authenticated — missing/invalid token or cookie |
67
+ | **403** | Authenticated but **not authorized** — RBAC grant missing (`me` not on the user’s role) |
68
+ | **404** | Feature gate off or wrong path/mount |
69
+ | **412** | Email verification required — grace period elapsed; verify or raise `verify_email_required_within` |
70
+ | **503** | Often blank `host_key` on experience-state routes; fix step 08 |
71
+ | Connection refused | Compose `command` not binding `0.0.0.0`, or published port mismatch |
72
+
73
+ Do not “fix” 403 by disabling RBAC. Grant `me` on `member`.
74
+
75
+ ## After `GET /me`
76
+
77
+ Attaching a Command Tower frontend is **not** this file. Follow [Local FE↔BE join](../../../artifacts/COMMANDTOWER_LOCAL_JOIN.md).
78
+
79
+ ## Stop
80
+
81
+ `GET /me` is **200** for a `member` user. Optional extras: [`13-optional.md`](13-optional.md). Required wrap-up: [`14-sanity-checks.md`](14-sanity-checks.md).