command_tower 0.17.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/me/inbox_controller.rb +16 -0
- data/app/deserializers/command_tower/deserializers/messaging/inbox.rb +85 -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 +43 -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/rendering/inbox_document_renderer.rb +53 -36
- 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.rb +5 -1
- 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 +47 -1
- 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/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 +3 -3
- data/docs/host_integration_guide.md +3 -281
- data/docs/initializing.md +37 -28
- data/docs/messaging_integration_guide.md +1 -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.18.0.md +35 -0
- data/docs/upgrades/README.md +2 -1
- data/lib/command_tower/authorization/default.yml +1 -0
- data/lib/command_tower/install/baseline.rb +2 -0
- data/lib/command_tower/version.rb +1 -1
- metadata +25 -2
|
@@ -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).
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# 13 — Optional (messaging, tests, client-version compatibility)
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
After `GET /me` works, add platform extras **only if the product needs them**. A platform-proof host may skip this file and run [`14-sanity-checks.md`](14-sanity-checks.md).
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- [`12-smoke-check.md`](12-smoke-check.md) passed
|
|
10
|
+
|
|
11
|
+
Do not start here. Messaging and CVC assume install, RBAC, and gates already exist.
|
|
12
|
+
|
|
13
|
+
## Messaging
|
|
14
|
+
|
|
15
|
+
When the host **emits** notifications or uses phone/Pushover:
|
|
16
|
+
|
|
17
|
+
- Catalog, channel policy, adapters, credentials: [`../messaging_integration_guide.md`](../messaging_integration_guide.md)
|
|
18
|
+
- Admin announcements: [`../admin_workspace.md`](../admin_workspace.md)
|
|
19
|
+
|
|
20
|
+
Complete steps 00–12 first. This bootstrap tree does not duplicate the messaging guide.
|
|
21
|
+
|
|
22
|
+
## Host tests
|
|
23
|
+
|
|
24
|
+
Shared FactoryBot definitions ship with the gem. In the host test boot path:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
require "command_tower/testing"
|
|
28
|
+
CommandTower::Testing.install!
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then `FactoryBot.modify` as needed. Contract: [`../testing.md`](../testing.md).
|
|
32
|
+
|
|
33
|
+
Run specs **in Compose**:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
make rspec
|
|
37
|
+
make spec SPEC=spec/path/to/file_spec.rb
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Do not run `rspec` on the Mac.
|
|
41
|
+
|
|
42
|
+
## Client-version compatibility (CVC)
|
|
43
|
+
|
|
44
|
+
If the frontend must block incompatible native/web builds, register CVC in the host (registry / initializer). Frontend recovery UX is a client concern; the backend must serve the compatibility contract the client expects.
|
|
45
|
+
|
|
46
|
+
Follow the gem’s registry comments and [`../extending.md`](../extending.md) for `config.registry` patterns. Do not invent a parallel version table.
|
|
47
|
+
|
|
48
|
+
## Stop
|
|
49
|
+
|
|
50
|
+
Either you skipped this step, or messaging/tests/CVC point at specialty docs and run via Make. Next: [`14-sanity-checks.md`](14-sanity-checks.md).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# 14 — Sanity checks
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Stop condition for **local** bootstrap. This is not a production launch checklist.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- Steps 00–12 complete
|
|
10
|
+
- Step 13 only if you opted in
|
|
11
|
+
|
|
12
|
+
## Checks
|
|
13
|
+
|
|
14
|
+
Run inside Compose (Make wrappers from [`01-docker-make-compose.md`](01-docker-make-compose.md)):
|
|
15
|
+
|
|
16
|
+
| Check | Command | Expect |
|
|
17
|
+
|-------|---------|--------|
|
|
18
|
+
| Doctor | `make doctor` | Exit 0 |
|
|
19
|
+
| Server | `make server` | API listens on the Compose port |
|
|
20
|
+
| Me smoke | [`12-smoke-check.md`](12-smoke-check.md) | `GET /me` **200** |
|
|
21
|
+
| Host specs (if you added them) | `make rspec` | Pass |
|
|
22
|
+
| RuboCop (if you added it) | `make rubocop` | Pass |
|
|
23
|
+
|
|
24
|
+
401 vs 403 vs 404: still [`12-smoke-check.md`](12-smoke-check.md).
|
|
25
|
+
|
|
26
|
+
## Not this manual
|
|
27
|
+
|
|
28
|
+
Do not treat the following as remaining bootstrap steps:
|
|
29
|
+
|
|
30
|
+
- Production deploys, systemd, reverse proxies, LXC
|
|
31
|
+
- Production secret distribution
|
|
32
|
+
- Multi-host clustering
|
|
33
|
+
- App-store / frontend release (see the Command Tower **frontend** bootstrap tree)
|
|
34
|
+
|
|
35
|
+
## After this tree
|
|
36
|
+
|
|
37
|
+
- Product routes and workflows: [`../extending.md`](../extending.md), [`../architecture.md`](../architecture.md)
|
|
38
|
+
- HTTP you did not smoke: [`../api_reference.md`](../api_reference.md)
|
|
39
|
+
- Gem upgrades: [`../upgrades/README.md`](../upgrades/README.md)
|
|
40
|
+
- Command Tower frontend on this API: [Local FE↔BE join](../../../artifacts/COMMANDTOWER_LOCAL_JOIN.md)
|
|
41
|
+
|
|
42
|
+
## Stop
|
|
43
|
+
|
|
44
|
+
Doctor is green and `GET /me` is **200**. Local Command Tower backend bootstrap is complete.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Bootstrap a new Command Tower backend host
|
|
2
|
+
|
|
3
|
+
Bring up a new Rails API host that **mounts** CommandTower, in order.
|
|
4
|
+
|
|
5
|
+
This directory is the host bring-up authority. Specialty contracts stay in sibling docs (`initializing`, `authorization`, `api_reference`, `extending`). This tree owns **order**, **which files the host authors** (including Docker and Make), and **how to invoke Rails** — always inside Compose.
|
|
6
|
+
|
|
7
|
+
Do **not** copy another product host’s tree. Do **not** copy [`../docker-compose.yaml`](../docker-compose.yaml) from this gem (that file is for **engine development**). The dummy app under [`../../rails_app/`](../../rails_app/) is an in-engine example for initializer and RBAC shape, not a product template.
|
|
8
|
+
|
|
9
|
+
## Docker-only
|
|
10
|
+
|
|
11
|
+
Every Ruby/Rails action runs inside Docker Compose.
|
|
12
|
+
|
|
13
|
+
**Host-machine tools:** Docker, the Docker Compose plugin, Make, Git. That is the entire local toolchain.
|
|
14
|
+
|
|
15
|
+
Do **not** run on the Mac (or any unsandboxed host OS):
|
|
16
|
+
|
|
17
|
+
- `gem install rails`
|
|
18
|
+
- `rails new`
|
|
19
|
+
- `bundle`
|
|
20
|
+
- `bin/rails` / `bin/rake`
|
|
21
|
+
- `rspec` / `rubocop`
|
|
22
|
+
|
|
23
|
+
Those names appear in this tree as **task names** inside the container, always invoked as `make …`.
|
|
24
|
+
|
|
25
|
+
**Chicken-and-egg:** author `Dockerfile`, `docker-compose.yml`, and `Makefile` **first** ([`01-docker-make-compose.md`](01-docker-make-compose.md)). `make build` produces a Ruby image. `make rails-new` runs `rails new` **in that image**. Only then pin the gem and install CommandTower.
|
|
26
|
+
|
|
27
|
+
## This is not deployment
|
|
28
|
+
|
|
29
|
+
This manual is **local bring-up**. It ends when the host runs in Compose and passes the checks in [`14-sanity-checks.md`](14-sanity-checks.md): `make doctor` and `GET /me` **200** for a member user.
|
|
30
|
+
|
|
31
|
+
It does **not** cover production deploys, systemd, reverse proxies, LXC, or how secrets are distributed in production. Do not add a deploy step to this tree without a separate decision.
|
|
32
|
+
|
|
33
|
+
## How to read
|
|
34
|
+
|
|
35
|
+
1. Follow **Implementation order** below. That list is the only sequence.
|
|
36
|
+
2. Open one numbered file per step. Later files assume earlier steps exist.
|
|
37
|
+
3. Each step states purpose, prerequisites, files to create, the procedure (`make` only), and the stop condition.
|
|
38
|
+
4. For field contracts, follow the linked specialty doc. Do not treat this tree as a second copy of those contracts.
|
|
39
|
+
|
|
40
|
+
A **platform-proof** host is: Docker/Make, Rails API in the container, CommandTower installed, `host_key`, host RBAC grants, feature gates, register → login → `GET /me`. Product domain and messaging adapters are optional ([`13-optional.md`](13-optional.md)).
|
|
41
|
+
|
|
42
|
+
Attaching a Command Tower **frontend** is a separate step after this tree: [Local FE↔BE join](../../../artifacts/COMMANDTOWER_LOCAL_JOIN.md).
|
|
43
|
+
|
|
44
|
+
## Implementation order
|
|
45
|
+
|
|
46
|
+
| Step | File | What you do |
|
|
47
|
+
|------|------|-------------|
|
|
48
|
+
| 00 | [`00-ownership.md`](00-ownership.md) | Engine vs host; schema; Docker-only; dummy is not a product template |
|
|
49
|
+
| 01 | [`01-docker-make-compose.md`](01-docker-make-compose.md) | Dockerfile, Compose, Makefile, `.dockerignore` |
|
|
50
|
+
| 02 | [`02-create-the-rails-app.md`](02-create-the-rails-app.md) | `make rails-new` (Rails 8 API) inside the container |
|
|
51
|
+
| 03 | [`03-pin-the-gem.md`](03-pin-the-gem.md) | `gem "command_tower"` from RubyGems (or git tag) |
|
|
52
|
+
| 04 | [`04-secrets-and-env.md`](04-secrets-and-env.md) | JWT / signup-session / password-recovery secrets in Compose |
|
|
53
|
+
| 05 | [`05-install-migrate-doctor.md`](05-install-migrate-doctor.md) | `command_tower:install`, migrate, doctor |
|
|
54
|
+
| 06 | [`06-mount-and-health.md`](06-mount-and-health.md) | Engine mount path; host healthz |
|
|
55
|
+
| 07 | [`07-execution-bases.md`](07-execution-bases.md) | Inherit CommandTower controller/job bases |
|
|
56
|
+
| 08 | [`08-initializer.md`](08-initializer.md) | `host_key`, cookies if needed, email knobs |
|
|
57
|
+
| 09 | [`09-rbac.md`](09-rbac.md) | Host `rbac_groups.yml` grants CT entity names |
|
|
58
|
+
| 10 | [`10-roles-and-gates.md`](10-roles-and-gates.md) | Default `member`; enable login / verify / reset gates |
|
|
59
|
+
| 11 | [`11-auth-client-path.md`](11-auth-client-path.md) | Bearer vs cookie+CSRF |
|
|
60
|
+
| 12 | [`12-smoke-check.md`](12-smoke-check.md) | Register → login → `GET /me` **200** |
|
|
61
|
+
| 13 | [`13-optional.md`](13-optional.md) | Optional: messaging, host tests, client-version compatibility |
|
|
62
|
+
| 14 | [`14-sanity-checks.md`](14-sanity-checks.md) | Stop: doctor + Me smoke; not deployment |
|
|
63
|
+
|
|
64
|
+
Step 13 is optional after 12.
|
|
65
|
+
|
|
66
|
+
## File index
|
|
67
|
+
|
|
68
|
+
Same files as the table. Filename numbers match the order list. If you add, remove, or reorder a step, change this README and the filenames together.
|
|
69
|
+
|
|
70
|
+
## Specialty docs this tree does not replace
|
|
71
|
+
|
|
72
|
+
| Need | Doc |
|
|
73
|
+
|------|-----|
|
|
74
|
+
| Attach a Command Tower frontend locally | [`../../../artifacts/COMMANDTOWER_LOCAL_JOIN.md`](../../../artifacts/COMMANDTOWER_LOCAL_JOIN.md) |
|
|
75
|
+
| What `command_tower:install` does, flags, schema, doctor | [`../initializing.md`](../initializing.md) |
|
|
76
|
+
| Layer map, workflow rule | [`../architecture.md`](../architecture.md) |
|
|
77
|
+
| Extension points | [`../extending.md`](../extending.md) |
|
|
78
|
+
| HTTP catalog | [`../api_reference.md`](../api_reference.md) |
|
|
79
|
+
| Route areas | [`../controllers.md`](../controllers.md) |
|
|
80
|
+
| RBAC | [`../authorization.md`](../authorization.md), [`../authentication_authorization_guide.md`](../authentication_authorization_guide.md) |
|
|
81
|
+
| Cookie / CSRF | [`../cookie_authentication_guide.md`](../cookie_authentication_guide.md) |
|
|
82
|
+
| Messaging | [`../messaging_integration_guide.md`](../messaging_integration_guide.md) |
|
|
83
|
+
| Testing factories | [`../testing.md`](../testing.md) |
|
|
84
|
+
| Admin Workspace | [`../admin_workspace.md`](../admin_workspace.md) |
|
|
85
|
+
| Principal capabilities | [`../principal_capabilities.md`](../principal_capabilities.md) |
|
|
86
|
+
| Audit registry | [`../audit.md`](../audit.md) |
|
|
87
|
+
| Eventing | [`../eventing.md`](../eventing.md) |
|
|
88
|
+
| Release upgrades | [`../upgrades/README.md`](../upgrades/README.md) |
|
|
89
|
+
|
|
90
|
+
Older entry: [`../host_integration_guide.md`](../host_integration_guide.md) is a pointer here.
|
|
@@ -37,6 +37,17 @@ end
|
|
|
37
37
|
| CSRF `same_site` / `secure` / `path` / `domain` | `nil` (inherit JWT cookie) |
|
|
38
38
|
| CSRF `ttl` | `7.days` |
|
|
39
39
|
|
|
40
|
+
### Two web apps on one hostname
|
|
41
|
+
|
|
42
|
+
The browser keys `ct_jwt` and `ct_csrf` by hostname. The port is not part of that key. Two Command Tower web apps on `localhost` overwrite each other’s session. Logout expires both cookies, so a shared CSRF name still logs the other app out on its next POST.
|
|
43
|
+
|
|
44
|
+
A second product sets both names. The frontend `session.csrfCookieName` matches `csrf.cookie_name`. Leave `header_name` as `X-CSRF-Token`. A host that runs alone keeps `ct_jwt` and `ct_csrf`.
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
config.jwt.cookie.name = "other_jwt"
|
|
48
|
+
config.jwt.cookie.csrf.cookie_name = "other_csrf"
|
|
49
|
+
```
|
|
50
|
+
|
|
40
51
|
## Token extraction order
|
|
41
52
|
|
|
42
53
|
1. `Authorization: Bearer <token>` header (wins when present)
|