@open-rlb/nestjs-amqp 2.0.3 → 2.0.5

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 (71) hide show
  1. package/README.md +4 -2
  2. package/amqp-lib/config/rabbitmq.config.d.ts +6 -0
  3. package/modules/acl/const.d.ts +1 -4
  4. package/modules/acl/const.js +1 -4
  5. package/modules/acl/const.js.map +1 -1
  6. package/modules/acl/models.d.ts +5 -7
  7. package/modules/acl/repository/acl-action.repository.d.ts +1 -5
  8. package/modules/acl/repository/acl-action.repository.js.map +1 -1
  9. package/modules/acl/repository/acl-grant.repository.d.ts +0 -1
  10. package/modules/acl/repository/acl-grant.repository.js.map +1 -1
  11. package/modules/acl/repository/acl-role.repository.d.ts +1 -5
  12. package/modules/acl/repository/acl-role.repository.js.map +1 -1
  13. package/modules/acl/services/acl-management.service.d.ts +6 -7
  14. package/modules/acl/services/acl-management.service.js +44 -61
  15. package/modules/acl/services/acl-management.service.js.map +1 -1
  16. package/modules/acl/services/acl.service.d.ts +1 -3
  17. package/modules/acl/services/acl.service.js +5 -47
  18. package/modules/acl/services/acl.service.js.map +1 -1
  19. package/modules/broker/broker.module.d.ts +2 -4
  20. package/modules/broker/broker.module.js +23 -5
  21. package/modules/broker/broker.module.js.map +1 -1
  22. package/modules/broker/config/route-discovery.config.d.ts +2 -0
  23. package/modules/broker/const.d.ts +1 -0
  24. package/modules/broker/const.js +2 -1
  25. package/modules/broker/const.js.map +1 -1
  26. package/modules/broker/services/broker.service.js +1 -1
  27. package/modules/broker/services/broker.service.js.map +1 -1
  28. package/modules/broker/services/route-discovery-publisher.service.js +7 -5
  29. package/modules/broker/services/route-discovery-publisher.service.js.map +1 -1
  30. package/modules/gateway-admin/config/gateway-admin.config.d.ts +4 -0
  31. package/modules/gateway-admin/const.d.ts +1 -1
  32. package/modules/gateway-admin/const.js +1 -1
  33. package/modules/gateway-admin/const.js.map +1 -1
  34. package/modules/gateway-admin/gateway-admin.module.d.ts +7 -1
  35. package/modules/gateway-admin/gateway-admin.module.js +13 -0
  36. package/modules/gateway-admin/gateway-admin.module.js.map +1 -1
  37. package/modules/gateway-admin/repository/auth-provider.repository.d.ts +3 -4
  38. package/modules/gateway-admin/repository/auth-provider.repository.js.map +1 -1
  39. package/modules/gateway-admin/services/gateway-auth.service.d.ts +3 -4
  40. package/modules/gateway-admin/services/gateway-auth.service.js +15 -23
  41. package/modules/gateway-admin/services/gateway-auth.service.js.map +1 -1
  42. package/modules/gateway-admin/services/gateway-metrics.service.d.ts +3 -0
  43. package/modules/gateway-admin/services/gateway-metrics.service.js +9 -0
  44. package/modules/gateway-admin/services/gateway-metrics.service.js.map +1 -1
  45. package/modules/gateway-admin/services/route-sync.service.d.ts +3 -1
  46. package/modules/gateway-admin/services/route-sync.service.js +14 -8
  47. package/modules/gateway-admin/services/route-sync.service.js.map +1 -1
  48. package/modules/proxy/services/http-handler.service.d.ts +3 -0
  49. package/modules/proxy/services/http-handler.service.js +28 -4
  50. package/modules/proxy/services/http-handler.service.js.map +1 -1
  51. package/package.json +5 -1
  52. package/schematics/nest-add/files/acl/src/cache/in-memory-acl-store.ts +54 -0
  53. package/schematics/nest-add/files/acl/src/modules/database/repository/acl.repository.ts +74 -0
  54. package/schematics/nest-add/files/db-core/src/modules/database/repository/in-memory-collection.ts +122 -0
  55. package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/gateway.repository.ts +151 -0
  56. package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/route-sync.repository.ts +15 -0
  57. package/schematics/nest-add/files/skills/rlb-amqp/SKILL.md +30 -5
  58. package/schematics/nest-add/files/skills/rlb-amqp/references/config-schema.md +182 -93
  59. package/schematics/nest-add/files/skills/rlb-amqp/references/gotchas.md +120 -79
  60. package/schematics/nest-add/files/skills/rlb-amqp-acl/SKILL.md +185 -0
  61. package/schematics/nest-add/files/skills/rlb-amqp-add-action/SKILL.md +49 -2
  62. package/schematics/nest-add/files/skills/rlb-amqp-add-route/SKILL.md +82 -19
  63. package/schematics/nest-add/files/skills/rlb-amqp-add-ws-event/SKILL.md +40 -18
  64. package/schematics/nest-add/files/skills/rlb-amqp-gateway-admin/SKILL.md +233 -0
  65. package/schematics/nest-add/files/skills/rlb-amqp-scaffold/SKILL.md +172 -42
  66. package/schematics/nest-add/index.js +612 -142
  67. package/schematics/nest-add/index.js.map +1 -1
  68. package/schematics/nest-add/index.ts +673 -241
  69. package/schematics/nest-add/init.schema.d.ts +10 -1
  70. package/schematics/nest-add/init.schema.ts +29 -3
  71. package/schematics/nest-add/schema.json +37 -8
@@ -8,38 +8,47 @@ description: Add a secure WebSocket event (or HTTP webhook) to the @open-rlb/nes
8
8
  Read first:
9
9
  - `.claude/skills/rlb-amqp/references/config-schema.md` (the `gateway.events[]` + `gateway.ws`
10
10
  sections)
11
- - `.claude/skills/rlb-amqp/references/gotchas.md` (WebSocket items 16–18, ACL item 15)
11
+ - `.claude/skills/rlb-amqp/references/gotchas.md` (WebSocket items 16–18, 26–27; ACL item 15)
12
12
 
13
- A WS event subscribes to an exchange/routingKey and fans the messages out to the connected
14
- clients of EVERY gateway instance. Secure it by default.
13
+ A WS event binds a broker `exchange`/`routingKey` to a named client-facing stream and fans
14
+ each message out to the connected clients of EVERY gateway instance. Secure it by default.
15
15
 
16
16
  ## Decide
17
17
 
18
18
  - **type**: `ws` (push to clients) or `http` (forward each message to a webhook `url`).
19
+ (`mqtt` is also accepted by the type union but `ws`/`http` are the supported paths.)
19
20
  - **source**: `exchange` + `routingKey` (the exchange must exist in `broker.exchanges[]`;
20
- `connection_name` must be set — gotcha 8).
21
+ each instance needs a distinct `connection_name` — gotcha 8).
22
+ - **client-facing name**: clients subscribe by `name`; messages arrive as `on<Name>`
23
+ (first letter capitalized — `chat` → `onChat`).
21
24
  - **security**:
22
25
  - `auth: <provider>` → the provider that verifies the connection token AND maps its claims
23
- for THIS event (at subscribe time). When set, a valid token is required to subscribe.
26
+ for THIS event (at subscribe time, memoized per provider). When set, a valid token is
27
+ required to subscribe.
24
28
  - `requireAuth: false` → makes `auth` optional (anonymous allowed; claims mapped if a token
25
29
  is present — handy with `scopeClaim`). Defaults to `true` when `auth` is set.
26
30
  - `roles: [...]` → ACL check (needs `IAclRoleService`); requires `auth` for the identity.
27
- - `scopeClaim` + `payloadKey` → per-user scoping: a client only receives messages where
31
+ - `scopeClaim` + `payloadKey` → per-user isolation: a client only receives messages where
28
32
  `payload[payloadKey] === claims[scopeClaim]`. `scopeClaim` is the MAPPED claim
29
33
  (with `headerPrefix`, e.g. `X-GTW-AUTH-USERID`). Without `payloadKey` it denies all
30
- (gotcha 16).
34
+ (gotcha 16). With `auth` but no `scopeClaim`/`payloadKey`, every authorized subscriber
35
+ gets ALL messages (warned at boot).
31
36
 
32
- > Auth/roles/scope are declared PER-EVENT. `gateway.ws` only holds connection-level limits
33
- > and heartbeat (no auth fields). Different events may use different providers.
37
+ > Auth/roles/scope are declared PER-EVENT. `gateway.ws` only holds connection-level limits,
38
+ > heartbeat, origin allowlist and message-size cap (no auth fields). Different events may use
39
+ > different providers.
34
40
 
35
41
  ## YAML fragments
36
42
 
37
43
  ```yaml
38
44
  gateway:
39
45
  ws: # connection-level only (optional)
40
- maxConnections: 5000
41
- maxSubscriptionsPerClient: 50
42
- heartbeatIntervalMs: 30000
46
+ maxConnections: 5000 # max concurrent connections this instance accepts
47
+ maxSubscriptionsPerClient: 50 # max active subscriptions per client
48
+ heartbeatIntervalMs: 30000 # ping/pong; also drops dead sockets + expired tokens
49
+ allowedOrigins: # reject cross-site handshakes; omit → all origins (logged)
50
+ - https://app.example.com
51
+ maxMessageBytes: 16384 # inbound client frame cap; larger frames dropped (default)
43
52
 
44
53
  events:
45
54
  - name: orders
@@ -49,8 +58,8 @@ gateway:
49
58
  auth: gateway-jwks # verifies token + maps claims for this event
50
59
  requireAuth: true # default true when auth is set; false → optional
51
60
  roles: [orders.read] # optional → needs IAclRoleService
52
- scopeClaim: X-GTW-AUTH-USERID # optional per-user scoping
53
- payloadKey: userId
61
+ scopeClaim: X-GTW-AUTH-USERID # optional per-user scoping (MAPPED claim)
62
+ payloadKey: userId # message field compared to scopeClaim
54
63
 
55
64
  - name: invoices # webhook variant
56
65
  type: http
@@ -58,6 +67,7 @@ gateway:
58
67
  routingKey: inv.#
59
68
  url: https://hooks.example.com/invoices
60
69
  method: POST
70
+ headers: { x-api-key: secret }
61
71
  timeout: 8000
62
72
  ```
63
73
 
@@ -75,19 +85,31 @@ broker:
75
85
  ## Required wiring to flag
76
86
 
77
87
  - The app bootstrap must register the WS adapter:
78
- `app.useWebSocketAdapter(new WsAdapter(app))`.
88
+ `app.useWebSocketAdapter(new WsAdapter(app))` (see
89
+ `sample/config-sample/gateway-in-memory/src/main.ts`).
79
90
  - `events[].auth` must reference a `jwt`/`jwks` provider; subscribing without a valid token
80
91
  yields `{ topic:'onError', data:{ event, error:'unauthorized' } }` (unless `requireAuth:false`).
81
- - `roles` `IAclRoleService` via `RLB_GTW_ACL_ROLE_SERVICE` (gotcha 15).
92
+ A failed role check yields `error:'forbidden'`.
93
+ - `roles` → `IAclRoleService` via `RLB_GTW_ACL_ROLE_SERVICE` in
94
+ `ProxyModule.forRootAsync({ providers: [...] })` (gotcha 15).
82
95
  - Do NOT add a fixed durable queue for the event — the lib creates a per-instance exclusive
83
- queue for fan-out (gotcha 17).
96
+ ephemeral auto-delete queue for fan-out (gotcha 17).
97
+ - WS sessions are bounded by the token `exp`: the socket is closed (`1008`) when the JWT
98
+ expires; long-lived sockets need refresh + reconnect (gotcha 26).
84
99
 
85
100
  ## Client snippet (for docs/testing)
86
101
 
87
102
  ```js
88
- const ws = new WebSocket('ws://localhost:3000', [token]); // token in subprotocol
103
+ // token in subprotocol — browsers can't set custom handshake headers (gotcha 18).
104
+ // single value = token; ['bearer', token] / ['jwt', token] pairs also accepted.
105
+ const ws = new WebSocket('ws://localhost:3000', [token]);
89
106
  ws.onopen = () => ws.send(JSON.stringify({ action: 'subscribe', topic: 'orders' }));
90
107
  ws.onmessage = (e) => console.log(JSON.parse(e.data)); // { topic:'onOrders', data } | { topic:'onError', ... }
108
+ // unsubscribe: ws.send(JSON.stringify({ action: 'unsubscribe', topic: 'orders' }));
91
109
  ```
92
110
 
111
+ > An optional `select: { key: value }` in the subscribe frame is a client-side filter
112
+ > (forwarded only when `payload[key] === value` for every key). It is INTERSECTED with the
113
+ > server-enforced `scopeClaim` isolation — a `select` can never widen what a user receives.
114
+
93
115
  Output the YAML fragments (with parent paths) and the bootstrap/ACL items still required.
@@ -0,0 +1,233 @@
1
+ ---
2
+ name: rlb-amqp-gateway-admin
3
+ description: Drive the @open-rlb/nestjs-amqp gateway-admin management plane and route auto-discovery. Use for DB-managed HTTP routes (gw-path-*), DB auth-providers CRUD (gw-auth-*, name-keyed PUT-upsert), gateway metrics (gw-metrics-get/series/points/track) and the gw-health probe, runtime route reload (gw-reload via the broadcast control topic), the in-proxy metrics hook (RLB_GTW_METRICS_HOOK), and wiring GatewayAdminModule.forRoot/forRootAsync. Also covers route auto-discovery: publisher (broker.routeDiscovery serviceName/publishOnBoot/exchange/queue) vs consumer (GatewayAdminModule routeDiscovery exchange/queue).
4
+ ---
5
+
6
+ # Gateway-admin module + route auto-discovery
7
+
8
+ Read first:
9
+ - `docs/gateway-admin.md` (full reference)
10
+ - `sample/config-sample/gateway-admin.yaml` (annotated YAML for every action)
11
+ - `sample/config-sample/gateway-db/` (runnable wiring; `apps/gateway-2` was retired)
12
+
13
+ The gateway-admin module is the gateway's management plane: DB-stored routes,
14
+ DB auth-providers, metrics, runtime reload, and the consumer side of route
15
+ auto-discovery. All handlers bind to the topic `rlb-gateway-admin`
16
+ (`GATEWAY_ADMIN_TOPIC`). The topic NAME and every action string are
17
+ **decorator-bound — NOT configurable**. You drive them by adding
18
+ `gateway.paths[]` that forward to that topic/action.
19
+
20
+ ## Fixed vs configurable
21
+
22
+ - FIXED (write exactly): topic names `rlb-gateway-admin`, control action
23
+ `gw-reload`, and all action strings `gw-path-*` / `gw-auth-*` / `gw-metrics-*`
24
+ / `gw-health` (from `GW_ADMIN_ACTIONS` + `GW_RELOAD_ACTION`).
25
+ - CONFIGURABLE: each topic's exchange/queue/routingKey, and the route-discovery
26
+ exchange/queue (defaults `rlb-route-discovery` / `rlb-route-sync`) — which must
27
+ match on the publisher AND consumer sides.
28
+
29
+ ## Nest wiring — `GatewayAdminModule`
30
+
31
+ `forRoot(providers, options)`: repo bindings FIRST, options SECOND. You supply
32
+ the four repositories (any store); the module ships the services and wires
33
+ `RouteSyncService` internally. Tokens are re-exported from `@open-rlb/nestjs-amqp`.
34
+
35
+ ```ts
36
+ GatewayAdminModule.forRoot([
37
+ { provide: HttpPathRepository, useExisting: MongoHttpPathRepository },
38
+ { provide: AuthProviderRepository, useExisting: MongoAuthProviderRepository },
39
+ { provide: HttpMetricRepository, useExisting: MongoHttpMetricRepository },
40
+ { provide: RouteSyncLogRepository, useExisting: MongoRouteSyncLogRepository }, // route-discovery journal
41
+ ]),
42
+ ```
43
+
44
+ Use `forRootAsync` to resolve the **consumer-side** `routeDiscovery { exchange, queue }`
45
+ from config (see Route auto-discovery). Exports `GatewayPathService`,
46
+ `GatewayAuthService`, `GatewayMetricsService`.
47
+
48
+ ## Broker topic + queue (required)
49
+
50
+ ```yaml
51
+ broker:
52
+ queues:
53
+ - name: rlb-gateway-admin
54
+ exchange: rlb
55
+ routingKey: rlb-gateway-admin
56
+ createQueueIfNotExists: true
57
+ options: { durable: true }
58
+ topics:
59
+ - name: rlb-gateway-admin # MUST be this name
60
+ mode: rpc
61
+ queue: rlb-gateway-admin
62
+ exchange: rlb
63
+ routingKey: rlb-gateway-admin
64
+ - name: rlb-gateway-control # name is yours; must match gateway.reloadTopic + gw-reload path
65
+ mode: broadcast
66
+ exchange: rlb
67
+ routingKey: rlb-gateway-control
68
+ ```
69
+
70
+ Point `loadConfig.paths` at the export responder so DB routes merge with YAML
71
+ `gateway.paths` on boot and on every reload (merged static-before-param):
72
+
73
+ ```yaml
74
+ gateway:
75
+ loadConfig:
76
+ paths: { topic: rlb-gateway-admin, action: gw-path-export }
77
+ reloadTopic: rlb-gateway-control
78
+ metrics: { topic: rlb-gateway-admin, action: gw-metrics-track } # auto-emit track per request
79
+ ```
80
+
81
+ ## Route management — `gw-path-*` (id-keyed)
82
+
83
+ `gw-path-create` is a **POST** (DB paths have an `id`) — unlike the name-keyed
84
+ auth/ACL resources. `create` rejects a `(method, path)` collision (409).
85
+ `export` returns enabled paths, ordered, and feeds `loadConfig.paths`.
86
+
87
+ | Method | Path | action | dataSource | Notes |
88
+ | --- | --- | --- | --- | --- |
89
+ | POST | `/admin/paths` | `gw-path-create` | body | needs `name,method,path,topic`; 409 on collision |
90
+ | GET | `/admin/paths` | `gw-path-list` | query | `?page=&limit=` |
91
+ | GET | `/admin/paths/export` | `gw-path-export` | query | enabled, ordered; used by `loadConfig.paths` |
92
+ | PUT | `/admin/paths` | `gw-path-update` | body | needs `id`; re-checks collisions |
93
+ | GET | `/admin/paths/get` | `gw-path-get` | query | `?id=` |
94
+ | DELETE | `/admin/paths` | `gw-path-delete` | body | `{ id }` |
95
+
96
+ ## Auth-provider management — `gw-auth-*` (name-keyed PUT-upsert)
97
+
98
+ No `id`, no POST — a single `PUT` creates-or-updates by `name`. These are the
99
+ DB-stored providers, ON TOP of the static `auth-providers[]` in YAML.
100
+
101
+ | Method | Path | action | dataSource | Notes |
102
+ | --- | --- | --- | --- | --- |
103
+ | GET | `/admin/auth` | `gw-auth-list` | query | `?page=&limit=` |
104
+ | PUT | `/admin/auth` | `gw-auth-update` | body | upsert by name; `{ name, type, ... }` |
105
+ | GET | `/admin/auth/get` | `gw-auth-get` | query | `?name=` |
106
+ | DELETE | `/admin/auth` | `gw-auth-delete` | body | `{ name }` |
107
+
108
+ `gw-auth-export` (dump all enabled) also exists; not in the sample YAML.
109
+
110
+ ## Metrics — `gw-metrics-*` + `gw-health`
111
+
112
+ | Method | Path | action | mode | dataSource | Returns |
113
+ | --- | --- | --- | --- | --- | --- |
114
+ | GET | `/admin/metrics` | `gw-metrics-get` | rpc | query | counters/route (`count,errorCount,avgDurationMs`); `?route=` |
115
+ | GET | `/admin/metrics/series` | `gw-metrics-series` | rpc | query | buckets: `?bucketMs=60000&from=&to=&method=&route=&name=` |
116
+ | GET | `/admin/metrics/points` | `gw-metrics-points` | rpc | query | raw points newest-first: `?method=&route=&from=&to=&limit=` |
117
+ | POST | `/admin/metrics/track` | `gw-metrics-track` | event | body | fire-and-forget sink (wired via `gateway.metrics`) |
118
+
119
+ `gw-health` → `{ status: 'ok' }`, a real 200 liveness probe (NOT a metrics dump):
120
+
121
+ ```yaml
122
+ - name: health
123
+ method: GET
124
+ path: /health
125
+ dataSource: query
126
+ topic: rlb-gateway-admin
127
+ action: gw-health
128
+ mode: rpc
129
+ ```
130
+
131
+ ### In-proxy metrics hook — `RLB_GTW_METRICS_HOOK`
132
+
133
+ Independent of the broker `gateway.metrics` sink: the gateway invokes a hook
134
+ once per request, after the response is flushed. Register under
135
+ `RLB_GTW_METRICS_HOOK` in `ProxyModule.forRootAsync`'s `providers`. Both sinks
136
+ can be active; the hook must not throw and should be cheap/async.
137
+
138
+ ```ts
139
+ export interface GatewayMetricsHook { track(p: GatewayMetricPoint): void | Promise<void>; }
140
+ // GatewayMetricPoint: { ts, method, route, name?, topic?, action?, mode?, status?, durationMs? }
141
+
142
+ ProxyModule.forRootAsync({ /* ... */ providers: [
143
+ { provide: RLB_GTW_METRICS_HOOK, useClass: InfluxMetricsHook },
144
+ ]}),
145
+ ```
146
+
147
+ (`sample/config-sample/gateway-db` ships an `InfluxMetricsHook` that is a no-op
148
+ until `INFLUX_URL/TOKEN/ORG` env are set.)
149
+
150
+ ## Runtime reload — `gw-reload`
151
+
152
+ The ONLY control action that forces a route rebuild. Published to the
153
+ **broadcast control topic** (`gateway.reloadTopic`, NOT `rlb-gateway-admin`),
154
+ `mode: event`. The control-topic subscriber ignores everything except
155
+ `gw-reload`, staying decoupled from other control traffic.
156
+
157
+ ```yaml
158
+ - name: gw-reload
159
+ method: POST
160
+ path: /admin/reload
161
+ dataSource: body
162
+ topic: rlb-gateway-control # the broadcast control topic
163
+ action: gw-reload
164
+ mode: event
165
+ ```
166
+
167
+ Seed DB routes via `POST /admin/paths`, then `POST /admin/reload` — no restart.
168
+
169
+ ## Route auto-discovery (publisher vs consumer)
170
+
171
+ A microservice announces its own `@BrokerHTTP`/`@BrokerAction` routes; the
172
+ gateway persists + registers them, no YAML edits. Two halves must agree on the
173
+ same exchange/queue (defaults `rlb-route-discovery` / `rlb-route-sync`).
174
+
175
+ ### Publisher (microservice → gateway) — `broker.routeDiscovery`
176
+
177
+ Lives in `BrokerModule`, so its config is INSIDE the broker block.
178
+
179
+ | Field | Default | Purpose |
180
+ | --- | --- | --- |
181
+ | `serviceName` | — | **Required to publish**; ownership key. PROMOTES to AMQP `connection_name` if none set. |
182
+ | `publishOnBoot` | `true` | Announce manifest on bootstrap (durable/persistent message; queue buffers it). |
183
+ | `exchange` | `rlb-route-discovery` | Fanout exchange the manifest is published to. |
184
+ | `queue` | `rlb-route-sync` | Durable shared work-queue the gateway consumes. |
185
+
186
+ ```yaml
187
+ # in the MICROSERVICE config.yaml:
188
+ broker:
189
+ routeDiscovery:
190
+ serviceName: demo-ms # required; also fills connection_name if unset
191
+ publishOnBoot: true
192
+ # exchange/queue default; override only to namespace per env (then match the consumer)
193
+ ```
194
+
195
+ Routes are declared `@BrokerHTTP` over a `@BrokerAction` method. The gateway must
196
+ still declare the microservice's broker topic so it can route forwarded calls.
197
+
198
+ ### Consumer (gateway ← microservice) — `GatewayAdminModule` `routeDiscovery`
199
+
200
+ Wired by `GatewayAdminModule` (NOT YAML). Asserts the fanout exchange, subscribes
201
+ to the durable queue (competing consumers), then per manifest: diffs vs DB scoped
202
+ to the publishing service, applies only changes (insert/update; soft-disable stale
203
+ to `enabled:false`; skip collisions — existing owner keeps `(method,path)`),
204
+ journals every event via `RouteSyncLogRepository`, and publishes `gw-reload` when
205
+ anything changed. Never throws (acks; no poison loop).
206
+
207
+ ```ts
208
+ GatewayAdminModule.forRootAsync({
209
+ imports: [ConfigModule], inject: [ConfigService],
210
+ useFactory: (c: ConfigService) => ({
211
+ routeDiscovery: {
212
+ exchange: c.get('routeDiscovery.exchange'), // default rlb-route-discovery
213
+ queue: c.get('routeDiscovery.queue'), // default rlb-route-sync — MUST match publishers
214
+ },
215
+ }),
216
+ providers: [ /* HttpPathRepository, AuthProviderRepository, HttpMetricRepository, RouteSyncLogRepository */ ],
217
+ }),
218
+ ```
219
+
220
+ The consumer has NO `serviceName` (it only receives). The exchange/queue MUST
221
+ match every publisher's `broker.routeDiscovery`.
222
+
223
+ ## Verify
224
+
225
+ - Topic `rlb-gateway-admin` (+ queue) declared; `reloadTopic` matches the
226
+ broadcast control topic name and the `gw-reload` path's `topic`.
227
+ - Action strings written exactly (`gw-path-*`, `gw-auth-*`, `gw-metrics-*`,
228
+ `gw-health`, `gw-reload`).
229
+ - `loadConfig.paths` → `gw-path-export`; `gateway.metrics` → `gw-metrics-track`.
230
+ - Route-discovery `exchange`/`queue` identical on publisher and consumer.
231
+ - All four repositories bound; `npm run build`.
232
+
233
+ See also: `rlb-amqp` (schema/gotchas) · `rlb-amqp-add-route` · `docs/gateway.md` · `docs/acl.md`
@@ -9,10 +9,83 @@ Read first:
9
9
  - `.claude/skills/rlb-amqp/references/config-schema.md`
10
10
  - `.claude/skills/rlb-amqp/references/gotchas.md`
11
11
 
12
- Decide the role: a **microservice** (only `@BrokerAction` handlers), a **gateway**
13
- (HTTP/WS exposure), or **both** in one app. Generate only the pieces needed.
12
+ Decide the role: a **microservice** (only `@BrokerAction` / `@BrokerHTTP` handlers, no
13
+ HTTP server) or a **gateway** (HTTP/WS exposure in front of microservices). Two paths:
14
+ the `nest add` schematic (fast, patches in place) or manual wiring. Canonical runnable
15
+ examples live under `sample/config-sample/` (`gateway-in-memory`, `gateway-db`,
16
+ `calculator.ms`) — mirror those, not the retired `apps/gateway-2`.
14
17
 
15
- ## 1. `src/config/config.loader.ts`
18
+ ---
19
+
20
+ ## Path A — `nest add` schematic (preferred)
21
+
22
+ From a NestJS project root:
23
+
24
+ ```bash
25
+ nest add @open-rlb/nestjs-amqp
26
+ ```
27
+
28
+ It **patches in place** (does not scaffold a new app): edits `src/app.module.ts` and
29
+ `src/main.ts`, creates `src/config/config.loader.ts` + `config/config.yaml`, copies
30
+ RUNNABLE in-memory repositories for the selected features, adds deps, and copies the
31
+ Claude skills.
32
+
33
+ ### Interactive flow
34
+
35
+ 1. **"Create a gateway (HTTP/WebSocket) configuration? y/N"**
36
+ 2. **YES** → checkbox of gateway features:
37
+ - `acl` — `AclModule` + ACL management/grant/check paths
38
+ - `gateway-admin` — `GatewayAdminModule` + DB-managed routes/auth-providers/metrics paths
39
+ - `route-reception` — gateway consumes routes auto-published by microservices
40
+ Then prompts for names (defaults shown): exchange `rlb`, ACL queue `rlb-acl`,
41
+ admin queue `rlb-gateway-admin`, control topic `rlb-gateway-control`,
42
+ route exchange `rlb-route-discovery`, route queue `rlb-route-sync`.
43
+ 3. **NO** (plain microservice) → checkbox:
44
+ - `auto-config-publish` — publish this service's `@BrokerHTTP` routes to the gateway
45
+ on boot (adds `broker.routeDiscovery`). Prompts service name + route exchange/queue.
46
+ 4. "Copy the Claude skills into .claude/skills? Y/n"
47
+
48
+ ### Non-interactive flags (CI / scripted)
49
+
50
+ Passing `--gatewayConfig` or any `--features` skips the prompts; everything else falls
51
+ back to `rlb-*` defaults.
52
+
53
+ ```bash
54
+ # Gateway with ACL + admin + route reception
55
+ nest add @open-rlb/nestjs-amqp \
56
+ --gatewayConfig --features acl --features gateway-admin --features route-reception
57
+
58
+ # Plain microservice that auto-publishes its @BrokerHTTP routes on boot
59
+ nest add @open-rlb/nestjs-amqp \
60
+ --gatewayConfig=false --features auto-config-publish \
61
+ --serviceName my-service --routeExchange rlb-route-discovery --routeQueue rlb-route-sync
62
+ ```
63
+
64
+ | Flag | Purpose |
65
+ | --- | --- |
66
+ | `--gatewayConfig` | `true` = gateway, `false` = microservice (default false non-interactive). |
67
+ | `--features <f>` | Repeatable. Gateway: `acl`, `gateway-admin`, `route-reception`. MS: `auto-config-publish`. |
68
+ | `--exchange` | Main AMQP exchange backing acl/admin queues. Default `rlb`. |
69
+ | `--aclQueue` / `--adminQueue` | Queues backing the fixed `rlb-acl` / `rlb-gateway-admin` topics. |
70
+ | `--controlTopic` | Broadcast control/reload topic. Default `rlb-gateway-control`. |
71
+ | `--routeExchange` / `--routeQueue` | Route-discovery exchange/queue. Defaults `rlb-route-discovery` / `rlb-route-sync` — **must match both publisher and gateway**. |
72
+ | `--serviceName` | Route-publish ownership key + AMQP `connection_name`. Default = project name. |
73
+ | `--skills` | Copy Claude skills. Default `true`; `--skills=false` to skip. |
74
+
75
+ > `app.module.ts` patch is idempotent (keys off `BrokerModule.forRootAsync`). If it can't
76
+ > locate `app.module.ts` / `main.ts` it warns and leaves you the manual wiring below.
77
+
78
+ After scaffolding, edit `config/config.yaml` (fill `<AMQP_URI>` / credentials) and replace
79
+ the `<APP_NAME>` `connection_name` placeholder. Then use `rlb-amqp-add-action` /
80
+ `rlb-amqp-add-route` / `rlb-amqp-add-ws-event` to grow the service.
81
+
82
+ ---
83
+
84
+ ## Path B — manual wiring
85
+
86
+ Mirrors `sample/config-sample/gateway-in-memory` (gateway) and `calculator.ms` (pure MS).
87
+
88
+ ### 1. `src/config/config.loader.ts`
16
89
 
17
90
  ```ts
18
91
  import { readFileSync } from 'fs';
@@ -20,23 +93,25 @@ import * as yaml from 'js-yaml';
20
93
  import { join } from 'path';
21
94
 
22
95
  const YAML_CONFIG_FILENAME = 'config/config.yaml';
96
+
23
97
  export default () =>
24
98
  yaml.load(readFileSync(join(process.cwd(), YAML_CONFIG_FILENAME), 'utf8')) as Record<string, any>;
25
99
  ```
26
100
 
27
- (`js-yaml` is a dependency of the config loader; ensure it's installed.)
101
+ (`js-yaml` + `@nestjs/config` are deps; the gateway also needs `@nestjs/axios`,
102
+ `@nestjs/platform-ws`, `@nestjs/websockets`, `ws`.)
28
103
 
29
- ## 2. `src/app.module.ts`
104
+ ### 2. `src/app.module.ts`
30
105
 
31
106
  ```ts
32
- import { HttpModule } from '@nestjs/axios';
107
+ import { HttpModule } from '@nestjs/axios'; // gateway only
33
108
  import { Module } from '@nestjs/common';
34
109
  import { ConfigModule, ConfigService } from '@nestjs/config';
35
- import { AppConfig, BrokerModule, BrokerTopic, GatewayConfig, ProxyModule } from '@open-rlb/nestjs-amqp';
36
- import { RabbitMQConfig } from '@open-rlb/nestjs-amqp/amqp-lib/config/rabbitmq.config';
37
- import { HandlerAuthConfig } from '@open-rlb/nestjs-amqp/modules/broker/config/handler-auth.config';
110
+ import {
111
+ AppConfig, BrokerModule, BrokerTopic, RabbitMQConfig,
112
+ GatewayConfig, HandlerAuthConfig, ProxyModule, // gateway only
113
+ } from '@open-rlb/nestjs-amqp';
38
114
  import yamlConfig from './config/config.loader';
39
- // import { MyActionService } from './my-action.service';
40
115
 
41
116
  @Module({
42
117
  imports: [
@@ -45,13 +120,15 @@ import yamlConfig from './config/config.loader';
45
120
  imports: [ConfigModule],
46
121
  inject: [ConfigService],
47
122
  useFactory: async (config: ConfigService) => ({
48
- options: config.get<RabbitMQConfig>('broker'),
49
- topics: config.get<BrokerTopic[]>('topics'),
123
+ // broker.routeDiscovery (publisher side), when used, lives INSIDE this `broker` block.
124
+ options: config.get<RabbitMQConfig>('broker')!,
125
+ topics: config.get<BrokerTopic[]>('topics')!,
50
126
  appOptions: config.get<AppConfig>('app'),
51
127
  }),
52
128
  }),
129
+
130
+ // --- gateway only: omit ProxyModule + HttpModule for a pure microservice ---
53
131
  HttpModule,
54
- // Gateway: auth-providers + gateway config live in ProxyModule (NOT BrokerModule).
55
132
  ProxyModule.forRootAsync({
56
133
  imports: [ConfigModule],
57
134
  inject: [ConfigService],
@@ -60,33 +137,59 @@ import yamlConfig from './config/config.loader';
60
137
  gatewayOptions: config.get<GatewayConfig>('gateway'),
61
138
  }),
62
139
  providers: [
63
- // { provide: RLB_GTW_ACL_ROLE_SERVICE, useClass: MyAclService }, // only if using `roles`
140
+ // Role-gated paths resolve the caller's roles in-process via this token (NO broker hop):
141
+ // { provide: RLB_GTW_ACL_ROLE_SERVICE, useExisting: AclService }, // required if any path uses `roles`
142
+ // Optional in-proxy per-request metrics hook (independent of gateway.metrics):
143
+ // { provide: RLB_GTW_METRICS_HOOK, useClass: InfluxMetricsHook },
64
144
  ],
65
145
  }),
66
146
  ],
67
- providers: [/* MyActionService */],
147
+ providers: [/* AppService — your @BrokerAction / @BrokerHTTP handlers */],
68
148
  })
69
149
  export class AppModule {}
70
150
  ```
71
151
 
72
- > Omit `ProxyModule`/`HttpModule` for a pure microservice with no HTTP/WS gateway.
152
+ > The gateway's `auth-providers` + `gateway` config belong to **`ProxyModule`**, not
153
+ > `BrokerModule`. For ACL add `AclModule.forRoot([...repos], { cache })` and bind
154
+ > `RLB_GTW_ACL_ROLE_SERVICE`; for DB routes/auth/metrics add
155
+ > `GatewayAdminModule.forRoot([...repos])` — see the `gateway-in-memory` sample and the
156
+ > ACL / Gateway Admin docs.
157
+
158
+ ### 3. `src/main.ts`
73
159
 
74
- ## 3. `src/main.ts`
160
+ **Gateway** (needs `rawBody` + `WsAdapter`):
75
161
 
76
162
  ```ts
163
+ import { ConfigService } from '@nestjs/config';
77
164
  import { NestFactory } from '@nestjs/core';
78
165
  import { WsAdapter } from '@nestjs/platform-ws';
79
166
  import { AppModule } from './app.module';
80
167
 
81
168
  async function bootstrap() {
82
- const app = await NestFactory.create(AppModule, { rawBody: true }); // rawBody needed if any path uses parseRaw
83
- app.useWebSocketAdapter(new WsAdapter(app)); // only if using the WS gateway
84
- await app.listen(3000, '0.0.0.0');
169
+ const app = await NestFactory.create(AppModule, { rawBody: true }); // rawBody: raw-body/webhook routes
170
+ app.useWebSocketAdapter(new WsAdapter(app)); // WS events
171
+ app.enableShutdownHooks();
172
+ const cfg = app.get(ConfigService).get<{ port?: number; host?: string }>('app');
173
+ await app.listen(Number(process.env.PORT) || cfg?.port || 3000, cfg?.host || '0.0.0.0');
85
174
  }
86
175
  bootstrap();
87
176
  ```
88
177
 
89
- ## 4. `config/config.yaml` (starter)
178
+ **Pure microservice** (no HTTP server — `init()`, not `listen()`):
179
+
180
+ ```ts
181
+ import { NestFactory } from '@nestjs/core';
182
+ import { AppModule } from './app.module';
183
+
184
+ async function bootstrap() {
185
+ const app = await NestFactory.create(AppModule);
186
+ app.enableShutdownHooks(); // drain in-flight RPC + close AMQP cleanly on SIGINT/SIGTERM
187
+ await app.init(); // @BrokerAction handlers start consuming once initialized
188
+ }
189
+ bootstrap();
190
+ ```
191
+
192
+ ### 4. `config/config.yaml` (starter)
90
193
 
91
194
  ```yaml
92
195
  app:
@@ -94,28 +197,29 @@ app:
94
197
  host: 0.0.0.0
95
198
  environment: development
96
199
 
97
- auth-providers: []
200
+ auth-providers: [] # gateway only — JWT/JWKS providers
98
201
 
99
202
  broker:
203
+ name: rabbitmq
100
204
  uri: "amqp://guest:guest@localhost:5672/"
101
- defaultRpcTimeout: 10000
102
205
  defaultSubscribeErrorBehavior: ack
206
+ defaultPublishErrorBehavior: reject
103
207
  connectionManagerOptions:
104
208
  heartbeatIntervalInSeconds: 60
105
209
  reconnectTimeInSeconds: 60
106
210
  connectionOptions:
107
211
  clientProperties:
108
- connection_name: my-service # REQUIRED for broadcast/WebSocket
212
+ connection_name: my-service # distinct per instance; needed for broadcast/WebSocket
109
213
  credentials: { mechanism: PLAIN, username: guest, password: guest }
110
214
  exchanges:
111
- - name: my-ex
215
+ - name: rlb
112
216
  type: direct
113
217
  createExchangeIfNotExists: true
114
218
  options: { durable: true }
115
219
  queues:
116
220
  - name: my-rpc-q
117
- exchange: my-ex
118
- routingKey: my.rpc
221
+ exchange: rlb
222
+ routingKey: my-rpc-q
119
223
  createQueueIfNotExists: true
120
224
  options: { durable: true }
121
225
 
@@ -123,38 +227,64 @@ topics:
123
227
  - name: my-rpc
124
228
  mode: rpc
125
229
  queue: my-rpc-q
230
+ exchange: rlb
231
+ routingKey: my-rpc-q
126
232
 
233
+ # --- gateway only ---
127
234
  gateway:
128
235
  mode: gateway
236
+ events: []
129
237
  paths:
130
- - name: ping
238
+ - name: health
131
239
  method: GET
132
- path: /ping
240
+ path: /health
133
241
  dataSource: query
134
- topic: my-rpc
135
- action: ping
242
+ topic: rlb-gateway-admin # gateway-admin gw-health → 200 { status: 'ok' }
243
+ action: gw-health
136
244
  mode: rpc
137
- events: []
138
245
  ```
139
246
 
140
- ## 5. Sample handler (optional)
247
+ ### Route auto-discovery (publisher side, optional)
248
+
249
+ A microservice can announce its `@BrokerHTTP` routes on boot. Add INSIDE the `broker`
250
+ block — `serviceName` is the ownership key AND fills `connection_name` when none is set
251
+ explicitly. `exchange`/`queue` default to `rlb-route-discovery` / `rlb-route-sync` and
252
+ must match the gateway's `GatewayAdminModule` `routeDiscovery { exchange, queue }`.
253
+
254
+ ```yaml
255
+ broker:
256
+ # ...
257
+ routeDiscovery:
258
+ serviceName: my-service
259
+ publishOnBoot: true
260
+ # exchange: rlb-route-discovery # override to namespace per env (must match the gateway)
261
+ # queue: rlb-route-sync
262
+ ```
263
+
264
+ ### 5. Sample handler (one method, both transports)
141
265
 
142
266
  ```ts
143
267
  import { Injectable } from '@nestjs/common';
144
- import { BrokerAction, BrokerParam } from '@open-rlb/nestjs-amqp';
268
+ import { BrokerAction, BrokerHTTP, BrokerParam } from '@open-rlb/nestjs-amqp';
145
269
 
146
270
  @Injectable()
147
- export class MyActionService {
148
- @BrokerAction('my-rpc', 'ping', 'rpc')
149
- async ping(@BrokerParam('body-full') data: any) {
150
- return { pong: true, echo: data };
271
+ export class AppService {
272
+ @BrokerAction('my-rpc', 'ping') // AMQP RPC on topic my-rpc, action ping
273
+ @BrokerHTTP('POST', '/ping', 'body') // route metadata for gateway auto-discovery
274
+ async ping(@BrokerParam('body', 'name') name: string) { // flat params, one decorator each
275
+ return { pong: true, name };
151
276
  }
152
277
  }
153
278
  ```
154
279
 
155
280
  ## Verify
156
- - topic/queue/exchange names line up (gotchas 5–7); `connection_name` set if needed (8).
157
- - `npm run build`, start the app with a reachable RabbitMQ, hit `/ping`.
281
+ - topic/queue/exchange names line up across `broker`/`topics`/paths (gotchas 5–7);
282
+ `connection_name` set if using broadcast/WebSocket (8).
283
+ - Route-discovery `exchange`/`queue` identical on publisher and gateway.
284
+ - Reload DB routes at runtime via the `gw-reload` action on the broadcast control topic
285
+ (default `rlb-gateway-control`).
286
+ - `npm run build`, start with a reachable RabbitMQ, hit `/health` (gateway) or publish to
287
+ the RPC topic (microservice).
158
288
 
159
- After scaffolding, use `rlb-amqp-add-action` / `rlb-amqp-add-route` / `rlb-amqp-add-ws-event`
160
- to grow the service.
289
+ After scaffolding, use `rlb-amqp-add-action` / `rlb-amqp-add-route` /
290
+ `rlb-amqp-add-ws-event` to grow the service.