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.
- checksums.yaml +4 -4
- data/README.md +8 -8
- data/app/controllers/command_tower/application_controller.rb +1 -0
- data/app/controllers/command_tower/me/inbox_controller.rb +16 -0
- data/app/controllers/concerns/command_tower/execution/client_compatibility_boundary.rb +39 -0
- data/app/deserializers/command_tower/deserializers/messaging/inbox.rb +85 -0
- data/app/errors/command_tower/errors/client_update_required_error.rb +27 -0
- data/app/jobs/command_tower/messaging/communications/produce_recipient_job.rb +2 -0
- data/app/models/command_tower/messaging/communication.rb +31 -0
- data/app/serializers/command_tower/serializers/messaging/inbox.rb +45 -1
- data/app/services/command_tower/email_theme/resolver.rb +45 -0
- data/app/services/command_tower/messaging/accept/coordinator.rb +16 -1
- data/app/services/command_tower/messaging/accept/persister.rb +13 -0
- data/app/services/command_tower/messaging/contract/mappers/communication_mapper.rb +7 -0
- data/app/services/command_tower/messaging/contract/results/communication_result.rb +1 -0
- data/app/services/command_tower/messaging/inbox/conversation_result.rb +31 -0
- data/app/services/command_tower/messaging/inbox/entry_result.rb +36 -0
- data/app/services/command_tower/messaging/inbox/reader.rb +231 -18
- data/app/services/command_tower/messaging/inbox.rb +4 -0
- data/app/services/command_tower/messaging/notification_types/declaration.rb +14 -1
- data/app/services/command_tower/messaging/rendering/channel_renderer.rb +44 -13
- data/app/services/command_tower/messaging/rendering/inbox_document.rb +131 -0
- data/app/services/command_tower/messaging/rendering/inbox_document_renderer.rb +224 -0
- data/app/services/command_tower/messaging/rendering/inbox_presentation_resolver.rb +52 -0
- data/app/services/command_tower/messaging/rendering/inbox_presentation_snapshot.rb +64 -0
- data/app/services/command_tower/messaging/rendering/template_resolver.rb +128 -0
- data/app/services/command_tower/messaging.rb +5 -1
- data/app/services/command_tower/services/client_compatibility/evaluate.rb +219 -0
- data/app/services/command_tower/services/messaging/communications/produce.rb +4 -0
- data/app/services/command_tower/services/messaging/communications/produce_many.rb +6 -0
- data/app/services/command_tower/services/messaging/inbox.rb +55 -2
- data/app/views/command_tower/email_verification_mailer/verify_email.html.erb +18 -17
- data/app/views/command_tower/messaging/rendering/email.html.erb +6 -5
- data/app/views/command_tower/password_reset_mailer/reset_password.html.erb +24 -23
- data/app/workflows/command_tower/workflows/auth/plain_text/login_workflow.rb +3 -0
- data/app/workflows/command_tower/workflows/auth/session/show_workflow.rb +3 -0
- data/app/workflows/command_tower/workflows/client_compatibility/evaluate_workflow.rb +83 -0
- data/app/workflows/command_tower/workflows/client_compatibility/recommendation_meta.rb +25 -0
- data/app/workflows/command_tower/workflows/messaging/communications/produce_recipient_workflow.rb +5 -1
- data/app/workflows/command_tower/workflows/messaging/inbox.rb +15 -1
- data/config/routes.rb +1 -0
- data/db/migrate/20261010000001_add_inbox_presentation_snapshot_to_messaging_communications.rb +7 -0
- data/db/migrate/20261010000002_add_conversation_identity_to_messaging_communications.rb +10 -0
- data/docs/api_reference.md +13 -2
- data/docs/authorization.md +1 -1
- data/docs/bootstrap/00-ownership.md +77 -0
- data/docs/bootstrap/01-docker-make-compose.md +267 -0
- data/docs/bootstrap/02-create-the-rails-app.md +75 -0
- data/docs/bootstrap/03-pin-the-gem.md +53 -0
- data/docs/bootstrap/04-secrets-and-env.md +59 -0
- data/docs/bootstrap/05-install-migrate-doctor.md +74 -0
- data/docs/bootstrap/06-mount-and-health.md +53 -0
- data/docs/bootstrap/07-execution-bases.md +38 -0
- data/docs/bootstrap/08-initializer.md +90 -0
- data/docs/bootstrap/09-rbac.md +83 -0
- data/docs/bootstrap/10-roles-and-gates.md +62 -0
- data/docs/bootstrap/11-auth-client-path.md +57 -0
- data/docs/bootstrap/12-smoke-check.md +81 -0
- data/docs/bootstrap/13-optional.md +50 -0
- data/docs/bootstrap/14-sanity-checks.md +44 -0
- data/docs/bootstrap/README.md +90 -0
- data/docs/cookie_authentication_guide.md +11 -0
- data/docs/extending.md +5 -4
- data/docs/host_integration_guide.md +3 -246
- data/docs/initializing.md +37 -28
- data/docs/messaging_integration_guide.md +39 -1
- data/docs/principal_capabilities.md +1 -1
- data/docs/upgrades/0.10.0.md +5 -5
- data/docs/upgrades/0.11.0.md +5 -5
- data/docs/upgrades/0.17.0.md +33 -0
- data/docs/upgrades/0.18.0.md +35 -0
- data/docs/upgrades/README.md +3 -1
- data/lib/command_tower/authorization/default.yml +1 -0
- data/lib/command_tower/client_compatibility/version.rb +45 -0
- data/lib/command_tower/client_compatibility.rb +23 -0
- data/lib/command_tower/configuration/config.rb +6 -0
- data/lib/command_tower/configuration/email_theme/config.rb +69 -0
- data/lib/command_tower/configuration/registry/client_compatibility/binding_definition.rb +42 -0
- data/lib/command_tower/configuration/registry/client_compatibility/config.rb +353 -0
- data/lib/command_tower/configuration/registry/client_compatibility/contract_definition.rb +16 -0
- data/lib/command_tower/configuration/registry/client_compatibility/entity_requirement_definition.rb +67 -0
- data/lib/command_tower/configuration/registry/client_compatibility/minimum_overrides.rb +46 -0
- data/lib/command_tower/configuration/registry/client_compatibility/platform_definition.rb +66 -0
- data/lib/command_tower/configuration/registry/config.rb +14 -0
- data/lib/command_tower/configuration/registry/inbox_presentations/config.rb +105 -0
- data/lib/command_tower/configuration/registry/inbox_presentations/presentation_definition.rb +71 -0
- data/lib/command_tower/current.rb +1 -0
- data/lib/command_tower/engine.rb +4 -0
- data/lib/command_tower/inbox_presentations.rb +19 -0
- data/lib/command_tower/install/baseline.rb +2 -0
- data/lib/command_tower/version.rb +1 -1
- 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).
|