@open-rlb/nestjs-amqp 2.0.4 → 2.0.6

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 (77) 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 +0 -1
  4. package/modules/acl/const.js +0 -1
  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-role.repository.d.ts +1 -5
  10. package/modules/acl/repository/acl-role.repository.js.map +1 -1
  11. package/modules/acl/services/acl-management.service.d.ts +2 -2
  12. package/modules/acl/services/acl-management.service.js +17 -20
  13. package/modules/acl/services/acl-management.service.js.map +1 -1
  14. package/modules/acl/services/acl.service.d.ts +1 -2
  15. package/modules/acl/services/acl.service.js +5 -21
  16. package/modules/acl/services/acl.service.js.map +1 -1
  17. package/modules/broker/broker.module.d.ts +2 -4
  18. package/modules/broker/broker.module.js +23 -5
  19. package/modules/broker/broker.module.js.map +1 -1
  20. package/modules/broker/config/decorator-paths.d.ts +1 -0
  21. package/modules/broker/config/decorator-paths.js +34 -4
  22. package/modules/broker/config/decorator-paths.js.map +1 -1
  23. package/modules/broker/config/route-discovery.config.d.ts +2 -0
  24. package/modules/broker/const.d.ts +1 -0
  25. package/modules/broker/const.js +2 -1
  26. package/modules/broker/const.js.map +1 -1
  27. package/modules/broker/decorators/broker-action.decorator.d.ts +2 -1
  28. package/modules/broker/decorators/broker-action.decorator.js +2 -2
  29. package/modules/broker/decorators/broker-action.decorator.js.map +1 -1
  30. package/modules/broker/services/broker.service.js +1 -1
  31. package/modules/broker/services/broker.service.js.map +1 -1
  32. package/modules/broker/services/metadata-scanner.service.js +11 -2
  33. package/modules/broker/services/metadata-scanner.service.js.map +1 -1
  34. package/modules/broker/services/route-discovery-publisher.service.js +7 -5
  35. package/modules/broker/services/route-discovery-publisher.service.js.map +1 -1
  36. package/modules/gateway-admin/config/gateway-admin.config.d.ts +4 -0
  37. package/modules/gateway-admin/const.d.ts +1 -1
  38. package/modules/gateway-admin/const.js +1 -1
  39. package/modules/gateway-admin/const.js.map +1 -1
  40. package/modules/gateway-admin/gateway-admin.module.d.ts +7 -1
  41. package/modules/gateway-admin/gateway-admin.module.js +13 -0
  42. package/modules/gateway-admin/gateway-admin.module.js.map +1 -1
  43. package/modules/gateway-admin/repository/auth-provider.repository.d.ts +3 -4
  44. package/modules/gateway-admin/repository/auth-provider.repository.js.map +1 -1
  45. package/modules/gateway-admin/services/gateway-auth.service.d.ts +3 -4
  46. package/modules/gateway-admin/services/gateway-auth.service.js +15 -23
  47. package/modules/gateway-admin/services/gateway-auth.service.js.map +1 -1
  48. package/modules/gateway-admin/services/gateway-metrics.service.d.ts +3 -0
  49. package/modules/gateway-admin/services/gateway-metrics.service.js +9 -0
  50. package/modules/gateway-admin/services/gateway-metrics.service.js.map +1 -1
  51. package/modules/gateway-admin/services/route-sync.service.d.ts +3 -1
  52. package/modules/gateway-admin/services/route-sync.service.js +14 -8
  53. package/modules/gateway-admin/services/route-sync.service.js.map +1 -1
  54. package/modules/proxy/services/http-handler.service.d.ts +3 -0
  55. package/modules/proxy/services/http-handler.service.js +27 -3
  56. package/modules/proxy/services/http-handler.service.js.map +1 -1
  57. package/package.json +5 -1
  58. package/schematics/nest-add/files/acl/src/cache/in-memory-acl-store.ts +54 -0
  59. package/schematics/nest-add/files/acl/src/modules/database/repository/acl.repository.ts +74 -0
  60. package/schematics/nest-add/files/db-core/src/modules/database/repository/in-memory-collection.ts +122 -0
  61. package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/gateway.repository.ts +151 -0
  62. package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/route-sync.repository.ts +15 -0
  63. package/schematics/nest-add/files/skills/rlb-amqp/SKILL.md +33 -5
  64. package/schematics/nest-add/files/skills/rlb-amqp/references/config-schema.md +182 -93
  65. package/schematics/nest-add/files/skills/rlb-amqp/references/gotchas.md +129 -79
  66. package/schematics/nest-add/files/skills/rlb-amqp-acl/SKILL.md +185 -0
  67. package/schematics/nest-add/files/skills/rlb-amqp-add-action/SKILL.md +87 -2
  68. package/schematics/nest-add/files/skills/rlb-amqp-add-route/SKILL.md +82 -19
  69. package/schematics/nest-add/files/skills/rlb-amqp-add-ws-event/SKILL.md +40 -18
  70. package/schematics/nest-add/files/skills/rlb-amqp-gateway-admin/SKILL.md +244 -0
  71. package/schematics/nest-add/files/skills/rlb-amqp-scaffold/SKILL.md +177 -42
  72. package/schematics/nest-add/index.js +612 -142
  73. package/schematics/nest-add/index.js.map +1 -1
  74. package/schematics/nest-add/index.ts +673 -241
  75. package/schematics/nest-add/init.schema.d.ts +10 -1
  76. package/schematics/nest-add/init.schema.ts +29 -3
  77. package/schematics/nest-add/schema.json +37 -8
@@ -1,104 +1,154 @@
1
1
  # Gotchas — bug-prone cases checklist
2
2
 
3
- Scan this before adding/changing a topic, queue, exchange, action, route, auth provider
4
- or WS event. Each item is a real failure mode in this codebase.
3
+ Scan this before adding/changing a topic, queue, exchange, action, route, auth provider,
4
+ WS event, or route-discovery wiring. Each item is a real failure mode in this codebase.
5
+ Ported from `docs/gotchas.md` (re-verified against post-2.0.5 code).
5
6
 
6
7
  ## Decorators & handlers
7
8
  1. **No destructuring in `@BrokerAction` parameters.** Param→message mapping parses the
8
- function source with a regex (`getParamNames`). `fn({a,b})` misaligns indices. Use flat
9
- params.
9
+ function source with a regex (`getParamNames`). `fn({a,b})` misaligns indices params
10
+ arrive `undefined`. Use flat params + an explicit `@BrokerParam` name on each.
10
11
  2. **Avoid default parameter values.** Only a basic `= value` strip exists
11
12
  (`removeDefaultsFromParams`); complex defaults misalign mapping. Always pass an explicit
12
13
  `name` to `@BrokerParam`.
13
- 3. **`(topic, action)` must be unique.** All `@BrokerAction` of a topic share ONE
14
- consumer/queue, dispatched by `action`. A duplicate `(topic, action)` overwrites the
15
- previous one silently.
14
+ 3. **`(topic, action)` must be unique.** All `@BrokerAction` of a topic share ONE consumer/queue,
15
+ dispatched by `action`. A duplicate `(topic, action)` overwrites the previous one silently —
16
+ no error, the old handler just stops being called.
16
17
  4. **Forwarded headers are UPPERCASE + prefixed.** Read `@BrokerParam('header',
17
- 'X-GTW-AUTH-USERID')`, not `'userId'`.
18
+ 'X-GTW-AUTH-USERID')`, not `'userId'`. The exact name = provider `headerPrefix` + `uidClaim`.
19
+ 5. **`handle`/`broadcast` handlers must return `void`.** A return value logs
20
+ `Subscribe handlers should only return void`. Only `rpc` handlers return data.
21
+ 6. **Two independent pairings on a method — `action` (http↔action) and `httpName` (auth↔route).**
22
+ Decorator order is never used; pair by name.
23
+ - **`@BrokerHTTP` ↔ `@BrokerAction`** via the `action` option: a method with multiple
24
+ `@BrokerAction`s requires each `@BrokerHTTP` to name its `action`; with one action it defaults.
25
+ - **`@BrokerAuth` ↔ `@BrokerHTTP` route** via `httpName` (= the route's `name`): auth is
26
+ **per ROUTE**, kept DECOUPLED from `@BrokerHTTP` (which carries NO auth). One `@BrokerHTTP`
27
+ auto-pairs its `@BrokerAuth` (no `name`/`httpName` needed); multiple `@BrokerHTTP` require each
28
+ to set a `name` and each `@BrokerAuth` to set the matching `httpName`. An `@BrokerAuth` whose
29
+ `httpName` matches no route is NOT applied and logs a WARNING at microservice startup.
30
+ - A route with no paired `@BrokerAuth` is **PUBLIC**. Two HTTP paths for the SAME action can now
31
+ carry DIFFERENT auth — pair each to its route by `name`.
18
32
 
19
33
  ## Topic ↔ queue ↔ exchange wiring
20
- 5. **The topic `name` must match everywhere**: `@BrokerAction`, `topics[].name`,
34
+ 7. **The topic `name` must match everywhere**: `@BrokerAction`, `topics[].name`,
21
35
  `requestData`/`publishMessage`, `gateway.paths[].topic` / `events[]`. Typo →
22
36
  `Topic X not found in configuration`.
23
- 6. **`mode: rpc`/`handle` need `topics[].queue` in `broker.queues[]`**, and that queue's
37
+ 8. **`mode: rpc`/`handle` need `topics[].queue` in `broker.queues[]`**, and that queue's
24
38
  `exchange` in `broker.exchanges[]`. In `handle` a missing queue throws NPE at boot
25
39
  (`queue.exchange`).
26
- 7. **Exchange `type: topic` → queue MUST have `routingKey`**, else boot throws
27
- `Queue ... has no routing key`.
28
- 8. **`broadcast` + WebSocket gateway require `connection_name`** (`clientProperties`), else
29
- throw.
40
+ 9. **Exchange `type: topic` → queue MUST have `routingKey`**, else boot throws
41
+ `Queue ... has no routing key`. (The samples use a `direct` exchange with matching keys.)
42
+ 10. **The gateway can only forward to topics it declares.** Route auto-discovery teaches the
43
+ gateway the HTTP route, NOT how to reach the microservice's broker topic. That topic (+ its
44
+ queue/exchange) must ALSO exist in the **gateway's own** `broker` config, or the forwarded
45
+ request fails with `Topic ... not found in configuration`.
46
+
47
+ ## connection_name (broadcast / WebSocket / route-discovery)
48
+ 11. **`broadcast` + WebSocket require `connection_name`** (`clientProperties.connection_name`,
49
+ or `broker.routeDiscovery.serviceName` which fills it when unset), else throw at boot.
50
+ 12. **Every instance needs a DISTINCT `connection_name`.** Sharing it makes RabbitMQ treat the
51
+ per-instance queues as one consumer group and **round-robin** broadcast/WS messages — reloads
52
+ land on "every other" instance, WS clients miss events delivered to the other one.
30
53
 
31
54
  ## RPC / timeout / errors
32
- 9. **RPC reply routing**: `requestData` resolves `replyTo` from `broker.replyQueues[exchange]`;
33
- absent → RabbitMQ direct-reply-to. Wrong exchange key in `replyQueues` → no reply → timeout.
34
- 10. **Handler exceptions don't throw on the consumer**: returned as `{success:false,error}`;
35
- `requestData` re-throws to the caller. Gateway status derives from `error.name` — give
36
- errors a meaningful `name`.
37
- 11. **Default RPC timeout 10s** (or `broker.defaultRpcTimeout`). Set `timeout` per path /
38
- per `requestData` call for slow RPCs.
55
+ 13. **Wrong `replyQueues` key → silent timeout.** `requestData` resolves `replyTo` from
56
+ `broker.replyQueues[exchange]`; absent → RabbitMQ direct-reply-to. Wrong exchange key → no
57
+ reply routed back the call just times out.
58
+ 14. **Handler exceptions don't crash the consumer.** Returned as `{success:false,error}`;
59
+ `requestData` re-throws on the caller side. Gateway HTTP status derives from `error.name` —
60
+ give errors a meaningful `name` (`BadRequestError`, `NotFoundError`, `ConflictError`,
61
+ `ForbiddenError`, `UnauthorizedError`); anything unrecognized 500.
62
+ 15. **Default RPC timeout 10s** (`broker.defaultRpcTimeout`). Override per route (`paths[].timeout`)
63
+ or per `requestData` call for slow RPCs.
39
64
 
40
65
  ## Gateway HTTP
41
- 12. **`parseRaw: true` needs `NestFactory.create(AppModule, { rawBody: true })`** or `$raw`
42
- is `undefined`.
43
- 13. **Route params win over body/query** (re-applied last). Watch key collisions (`:id`
44
- vs `body.id`).
45
- 14. **Uploads are in `$files`** (multer `.any()`); buffers are converted to binary strings —
46
- handle re-encoding carefully on the consumer side.
66
+ 16. **Boolean RPCs return `200` with `true`/`false`, not `204`.** A *defined* result — including
67
+ falsy `false`/`0`/`""` — is real content, sent as `200` + JSON. Only `null`/`undefined`
68
+ collapses to `204`. So `GET /acl/check?...` answers `200 false` for "no" — don't treat any
69
+ 2xx as "allowed", read the body. (The old "always 204" bug is fixed.)
70
+ 17. **`parseRaw: true` needs `NestFactory.create(AppModule, { rawBody: true })`** or `$raw` is
71
+ `undefined`.
72
+ 18. **Route params win over body/query** (merged in last). Watch key collisions (`:id` vs `body.id`).
73
+ 19. **Uploads live in `$files`** (multer `.any()`); buffers are converted to **binary strings** —
74
+ re-encode carefully on the consumer (`Buffer.from(str, 'binary')`).
75
+ 20. **`/health` is a tiny liveness probe.** Action `gw-health` → `{ status: 'ok' }` (a real 200),
76
+ NOT a metrics dump. Use `/admin/metrics*` (`gw-metrics-*`) for metrics.
47
77
 
48
78
  ## Auth / ACL
49
- 15. **`roles` on an HTTP path require an `IAclRoleService`** registered via
50
- `RLB_GTW_ACL_ROLE_SERVICE` in `ProxyModule.forRootAsync({ providers: [...] })`. The
51
- gateway check is **role-based** (`canUserDoGtw(path.roles, userId)`): `path.roles` lists
52
- ROLE NAMES and the user passes if they hold AT LEAST ONE (resource-agnostic primary
53
- filter). The provider only needs `uidClaim` (+ `headerPrefix`) to extract the userId —
54
- no topic/action.
55
- 16. **Two role-based ACL checks** on `rlb-acl` (both cached, inputs = userId + roles only):
56
- `acl-can-user-do-gtw` → `canUserDoGtw(roles, userId)` (gateway primary filter, OR,
57
- resource-agnostic) and `acl-can-user-do` → `canUserDo(roles, userId, resourceId)`
58
- (**ms-side**; a global grant OR a grant on that resource satisfies it — the resource is
59
- known only to the target ms).
60
- 17. **Auth-providers + gateway config are passed to `ProxyModule`** (`authOptions` /
61
- `gatewayOptions`), not `BrokerModule`. `BrokerModule` owns only `options`/`topics`/`appOptions`.
79
+ 21. **`roles` require `auth` on the same path/event.** No `auth` no identity → fails closed
80
+ (every request `403`, logged at boot). Always pair `roles: [...]` with `auth: <provider>`.
81
+ 22. **`roles` require an `IAclRoleService`** registered via `RLB_GTW_ACL_ROLE_SERVICE` in
82
+ `ProxyModule.forRootAsync({ providers: [...] })`. Missing deny (403). The gateway check is
83
+ **role-based, OR, resource-agnostic** (`canUserDoGtw(roles, userId)`): `roles` lists ROLE NAMES,
84
+ the user passes holding AT LEAST ONE. The provider only needs `uidClaim` (+ `headerPrefix`).
85
+ 23. **Two ACL check actions on `rlb-acl`** (both cached, both HTTP GET `200` true/false):
86
+ `acl-can-user-do-gtw` → `canUserDoGtw(roles, userId)` (gateway filter, OR, resource-agnostic,
87
+ `GET /acl/check`) and `acl-can-user-do` → `canUserDo(roles, userId, resource)` (**ms-side**;
88
+ a global grant OR a grant on that resource passes, `GET /acl/check-resource`).
89
+ 24. **Actions, roles & auth-providers are NAME-KEYED. PUT upserts; there is NO POST.** The `name`
90
+ IS the key (no id). `PUT` creates-or-updates, `GET` lists, `GET .../get?name=` reads one,
91
+ `DELETE` removes by `name`. The old id-based ACL CRUD and `POST`-create endpoints are GONE.
92
+ (Gateway-admin **paths** are the exception — they keep id-keyed CRUD and a POST create.)
93
+ 25. **`acl-grant` / `acl-revoke` both REQUIRE `userId` + `roles`** (optional `resourceId` +
94
+ `companyId`). `grant` MERGES roles into the single `(userId, resourceId)` record (idempotent).
95
+ `revoke` REMOVES exactly those roles and **deletes the record once it has no roles left**.
96
+ `revoke` without `roles` throws `400 roles are required` — to wipe a grant, revoke all its roles.
97
+ 26. **`companyId` is grouping metadata only.** It replaced `resourceBusinessId` and plays NO part
98
+ in authorization — it only groups resources in `acl-list-resources-by-user`. Targeting is by
99
+ `(userId, resourceId)` only. Both grant/revoke validate every role exists (unknown → `400`).
100
+ 27. **Removed actions:** `acl-list-by-user` and `acl-verify-access` no longer exist. Use
101
+ `acl-can-user-do` for resource-scoped checks and `acl-list-resources-by-user` to list resources.
102
+ 28. **Auth & gateway config go to `ProxyModule`** (`authOptions` / `gatewayOptions`), not
103
+ `BrokerModule`. `BrokerModule` owns only `options` / `topics` / `appOptions`.
62
104
 
63
- ## WebSocket
64
- 16. **Auth is per-event, not global.** `events[].auth` names the provider that verifies the
65
- connection token AND maps its claims for THAT event (at subscribe time, memoized per
66
- provider). `scopeClaim` references the MAPPED claim (with `headerPrefix`, e.g.
67
- `X-GTW-AUTH-USERID`), not the raw token claim. `payloadKey` is the event-payload field.
68
- `scopeClaim` without `payloadKey` denies everything (safe default). `requireAuth: false`
69
- on an event makes `auth` optional (anon allowed, claims mapped if a token is present).
70
- `gateway.ws` only carries connection-level limits/heartbeatno auth fields.
71
- 17. **Don't use a fixed durable queue for WS events.** The lib creates a per-instance
72
- exclusive ephemeral queue for fan-out; a shared queue makes instances compete and clients
73
- on one instance miss messages.
74
- 18. **Token transport is the subprotocol**: `new WebSocket(url, [token])`. Browsers can't set
75
- custom handshake headers.
105
+ ## Auth providers (hardening)
106
+ 29. **JWKS verifies TLS by default.** `httpsAllowUnauthorized: true` only for self-signed dev issuers.
107
+ 30. **`algorithms` is REQUIRED for `jwt`/`jwks`.** Omitting denies verification (algorithm-confusion
108
+ guard). For `jwks` only asymmetric algs are allowed (`RS*`/`ES*`/`PS*`); `HS*`/`none` rejected.
109
+ 31. **Define `jwtMap` or NO claims are forwarded.** Without it the token is still accepted
110
+ (`success:true`) but no identity headers go downstream — fail-safe, not a leak. Declare it to
111
+ emit `X-GTW-AUTH-USERID` and friends.
112
+ 32. **`str-compare`/`basic` PASS THROUGH when their secret is unset by design.** A `str-compare`
113
+ with no `secret`, or a `basic` with no `clientSecret`, authenticates EVERY request (effectively
114
+ open/disabled). Set the secret to enforce it.
115
+ 33. **Credential `mechanism` must be `PLAIN` | `EXTERNAL` | `AMQPLAIN`** (case-insensitive). Unknown
116
+ value leaves SASL `response` unset AMQP auth fails.
76
117
 
77
- ## Publish / event
78
- 19. **`publishMessage` is `async` `await` it** for the publisher-confirm guarantee and to
79
- catch failures. Un-awaited = fire-and-forget without guarantee.
80
- 20. **`handle`/`broadcast` handlers must return `void`**; a return value logs
81
- `Subscribe handlers should only return void`.
118
+ ## WebSocket
119
+ 34. **Auth is per-event, not global.** `events[].auth` names the provider that verifies the
120
+ connection token AND maps its claims for THAT event (memoized per provider at subscribe). `scopeClaim`
121
+ references the MAPPED claim (prefixed, e.g. `X-GTW-AUTH-USERID`), `payloadKey` is the payload
122
+ field. **`scopeClaim` without `payloadKey` denies everything** (safe default). `requireAuth:false`
123
+ makes `auth` optional (anon allowed; claims mapped if a token is present). `gateway.ws` carries
124
+ only connection-level limits/heartbeat — no auth fields.
125
+ 35. **Don't bind a WS event to a fixed durable queue.** The lib creates a per-instance exclusive
126
+ ephemeral queue for fan-out; a shared/durable queue makes instances compete and clients on one
127
+ instance miss messages delivered to another.
128
+ 36. **Token rides in the subprotocol**: `new WebSocket(url, [token])` (browsers can't set handshake
129
+ headers). The session is bounded by the JWT `exp` — closed with `1008` on expiry, nothing
130
+ delivered afterward. Long-lived clients need token refresh + reconnect.
131
+ 37. **Set `gateway.ws.allowedOrigins`** to reject cross-site handshakes; omitted → all Origins
132
+ accepted (logged at boot). `maxMessageBytes` (default 16384) drops oversized client frames.
82
133
 
83
- ## TLS / credentials / provider hardening
84
- 21. **JWKS verifies TLS by default.** `httpsAllowUnauthorized: true` only for self-signed dev
85
- issuers.
86
- 22. **Credential `mechanism`**: `PLAIN` | `EXTERNAL` | `AMQPLAIN` (case-insensitive). Unknown
87
- value leaves `response` unset → auth fails.
88
- 23. **`algorithms` is REQUIRED for `jwt`/`jwks`.** If omitted, verification is denied
89
- (algorithm-confusion guard). For `jwks` only asymmetric algs are allowed (RS*/ES*/PS*);
90
- `HS*`/`none` are rejected.
91
- 24. **`str-compare`/`basic` PASS THROUGH when their secret is unset.** A `str-compare`
92
- without `secret` or a `basic` without `clientSecret` treats every request as authenticated
93
- (provider effectively open/disabled — by design). Set the secret to actually enforce it.
94
- 25. **Define `jwtMap`.** Without it NO claims are forwarded (the token is still accepted,
95
- `success:true`): the gateway fails safe instead of leaking the whole payload. Declare it
96
- to forward identity headers (e.g. `X-GTW-AUTH-USERID`).
134
+ ## Publish / events
135
+ 38. **`publishMessage` is `async` `await` it** for the publisher-confirm guarantee and to catch
136
+ failures. Un-awaited = fire-and-forget without guarantee. (For an `event`-mode route the gateway
137
+ awaits the confirm before returning the 2xx the success status is not optimistic.)
97
138
 
98
- ## WebSocket session/transport security
99
- 26. **WS sessions are bounded by the token `exp`.** The connection is closed (`1008`) when the
100
- JWT expires; no delivery happens afterward. Long-lived sockets need token refresh +
101
- reconnect.
102
- 27. **Set `gateway.ws.allowedOrigins`** to reject cross-site handshakes; if omitted, all
103
- Origins are accepted (logged at boot). `maxMessageBytes` (default 16384) drops oversized
104
- client frames.
139
+ ## Reload & route auto-discovery
140
+ 39. **The only reload action is `gw-reload`** (`GW_RELOAD_ACTION`). The control-topic subscriber
141
+ rebuilds routes ONLY for `gw-reload`; every other message on the control topic is ignored.
142
+ Seed then reload: `POST /admin/paths` → `POST /admin/reload` (publishes `gw-reload` on
143
+ `rlb-gateway-control`). No restart. Concurrent reloads are coalesced into one extra pass.
144
+ 40. **Route-discovery config is SPLIT; exchange/queue MUST match on both sides.**
145
+ - **Publisher (microservice):** `broker.routeDiscovery { serviceName, publishOnBoot, exchange?, queue? }`.
146
+ `serviceName` is required to publish and also fills `connection_name` when unset.
147
+ - **Consumer (gateway):** `GatewayAdminModule` `routeDiscovery { exchange?, queue? }` (NEST code,
148
+ no `serviceName` — the gateway only receives).
149
+ Both default to `exchange: rlb-route-discovery` / `queue: rlb-route-sync`. Override only to
150
+ namespace per env — but set the SAME values on BOTH sides or manifests never reach the gateway.
151
+ 41. **Topic NAMES `rlb-acl` / `rlb-gateway-admin` / `rlb-gateway-control` and all action strings
152
+ are decorator-bound and NOT configurable.** Only exchange/queue/routingKey and the
153
+ route-discovery exchange/queue are. The route-sync handler never throws (logs + acks, no poison
154
+ loop); an empty manifest soft-disables a service's existing routes (and logs a warning).
@@ -0,0 +1,185 @@
1
+ ---
2
+ name: rlb-amqp-acl
3
+ description: Manage role-based access control (ACL) with @open-rlb/nestjs-amqp — actions, roles, grants/revokes, and "can user do X" checks. Use when wiring AclModule, gating gateway routes by roles, granting/revoking a user's roles, listing a user's resources, or answering authorization/permission questions (roles, grants, acl-check).
4
+ ---
5
+
6
+ # Manage ACL (@open-rlb/nestjs-amqp)
7
+
8
+ Read first when you need depth:
9
+ - `docs/acl.md` (model, wiring, URL table)
10
+ - `libs/rlb-nestjs-amqp/src/modules/acl/const.ts` (`ACL_ACTIONS`, `ACL_TOPIC`)
11
+ - `sample/config-sample/acl.yaml` (annotated broker + gateway reference)
12
+ - `sample/config-sample/gateway-in-memory/src/app.module.ts` (forRoot wiring)
13
+
14
+ Use when: managing **actions/roles/grants**, wiring `AclModule`, role-gating routes
15
+ (`roles: [...]`), or answering "can user do X".
16
+
17
+ ## Model (3 entities)
18
+
19
+ - **Action** — atomic capability (`read-doc`). Name-keyed.
20
+ - **Role** — bundle of action names (`editor = [read-doc, write-doc]`). Name-keyed.
21
+ - **Grant** — binds a `userId` → role names; one record per `(userId, resourceId)`.
22
+ - **Checks** match on **roles, never action strings**.
23
+
24
+ ## Decorator-bound (NOT configurable)
25
+
26
+ Topic NAME `rlb-acl` (`ACL_TOPIC`) and every action string are bound in the library —
27
+ reference them literally. The queue / exchange / routingKey that carry the topic ARE yours.
28
+
29
+ `ACL_ACTIONS`: `acl-action-list`, `acl-action-get`, `acl-action-update`,
30
+ `acl-action-delete`, `acl-role-list`, `acl-role-get`, `acl-role-update`,
31
+ `acl-role-delete`, `acl-grant`, `acl-revoke`, `acl-can-user-do-gtw`,
32
+ `acl-can-user-do`, `acl-list-resources-by-user`, `acl-invalidate`.
33
+
34
+ > **Removed in 2.0.5:** `acl-list-by-user`, `acl-verify-access`, `acl-create` /
35
+ > id-based ACL CRUD. Entities are name-keyed: **PUT upserts, no POST.**
36
+
37
+ ## Actions & roles — name-keyed CRUD
38
+
39
+ No id, no POST. `PUT` upserts by `name` (idempotent), `GET` lists (`?page=&limit=`),
40
+ `GET …/get?name=` reads one, `DELETE` removes by `name`. Role upsert: every referenced
41
+ action must already exist (else **400**).
42
+
43
+ ## Grants — dual grant/revoke
44
+
45
+ One record per `(userId, resourceId)`. Both ops **require `userId` + `roles`**;
46
+ `resourceId` + `companyId` are **optional**.
47
+
48
+ - `acl-grant` — merges roles into the pair (creates if absent; idempotent).
49
+ - `acl-revoke` — removes roles; deletes the record once empty.
50
+ - Both validate every role exists (unknown role → **400**) and invalidate the user's cache.
51
+ - `companyId` (replaced `resourceBusinessId`) is **grouping metadata only** — it groups
52
+ `acl-list-resources-by-user` output and plays **no part** in authorization.
53
+
54
+ ## Checks — GET → 200 with `true`/`false`
55
+
56
+ `false` is real content; only `null`/`undefined` collapses to 204. Both return `false`
57
+ (never throw) on missing input or error.
58
+
59
+ - `acl-can-user-do-gtw` — resource-**agnostic**, the gateway's primary filter. `true` if
60
+ the user holds **≥1** requested role. Query: `?userId=&roles=user&roles=admin`.
61
+ - `acl-can-user-do` — resource-**scoped**: `true` if a **global** grant OR a grant bound
62
+ to that exact `resource` gives a matching role. Query: `?userId=&roles=admin&resource=doc-1`.
63
+ Normally called over the broker by the owning microservice.
64
+ - `acl-list-resources-by-user` — **auth-gated** (needs `auth`, no roles): reads `userId`
65
+ from the forwarded `X-GTW-AUTH-USERID` header; lists accessible resources grouped by
66
+ `companyId` with resolved actions.
67
+
68
+ ## Nest wiring
69
+
70
+ Backend — `AclModule.forRoot([bindings], { cache })`. Bind the abstract repo tokens to
71
+ your concrete impls + optional L2 store; second arg carries TTLs. Module is **global**,
72
+ exports `AclService` + `AclCacheService`.
73
+
74
+ ```ts
75
+ import {
76
+ AclModule, AclActionRepository, AclRoleRepository, AclGrantRepository,
77
+ RLB_ACL_CACHE_STORE,
78
+ } from '@open-rlb/nestjs-amqp';
79
+
80
+ AclModule.forRoot(
81
+ [
82
+ { provide: AclActionRepository, useClass: MyAclActionRepository },
83
+ { provide: AclRoleRepository, useClass: MyAclRoleRepository },
84
+ { provide: AclGrantRepository, useClass: MyAclGrantRepository },
85
+ { provide: RLB_ACL_CACHE_STORE, useClass: MyRedisAclCacheStore }, // OPTIONAL L2 (omit → RAM-only)
86
+ ],
87
+ { cache: { ramTtlMs: 30_000, l2TtlSec: 600 } }, // L1 RAM (default 30000) / L2 (default 600s)
88
+ );
89
+ ```
90
+
91
+ Gateway side — let route `roles: [...]` filters run **in-process** (no broker hop) by
92
+ binding the gateway token to the same `AclService`:
93
+
94
+ ```ts
95
+ import { ProxyModule, AclService, RLB_GTW_ACL_ROLE_SERVICE } from '@open-rlb/nestjs-amqp';
96
+
97
+ ProxyModule.forRoot({
98
+ providers: [{ provide: RLB_GTW_ACL_ROLE_SERVICE, useExisting: AclService }],
99
+ });
100
+ ```
101
+
102
+ Same process → `useExisting`. Separate services → gateway RPCs `acl-can-user-do-gtw` on
103
+ `rlb-acl` instead. A route's `roles` are ROLE NAMES; the user passes with **≥1**.
104
+
105
+ ## YAML — topic + queue (names fixed, transport yours)
106
+
107
+ ```yaml
108
+ broker:
109
+ queues:
110
+ - name: rlb-acl # consumed by the ACL backend handlers
111
+ exchange: rlb
112
+ routingKey: rlb-acl
113
+ createQueueIfNotExists: true
114
+ options: { durable: true }
115
+ topics:
116
+ - name: rlb-acl # ACL_TOPIC — must match exactly
117
+ mode: rpc
118
+ queue: rlb-acl
119
+ exchange: rlb
120
+ routingKey: rlb-acl
121
+ ```
122
+
123
+ ## Gateway paths[] — full ACL table
124
+
125
+ Every path forwards to topic `rlb-acl`, `mode: rpc`. `name` is a free label; `action` is
126
+ the fixed library string.
127
+
128
+ | name | method | path | dataSource | action |
129
+ |---|---|---|---|---|
130
+ | acl-action-list | GET | /acl/actions | query | acl-action-list |
131
+ | acl-action-get | GET | /acl/actions/get | query | acl-action-get |
132
+ | acl-action-upsert | PUT | /acl/actions | body | acl-action-update |
133
+ | acl-action-delete | DELETE | /acl/actions | body | acl-action-delete |
134
+ | acl-role-list | GET | /acl/roles | query | acl-role-list |
135
+ | acl-role-get | GET | /acl/roles/get | query | acl-role-get |
136
+ | acl-role-upsert | PUT | /acl/roles | body | acl-role-update |
137
+ | acl-role-delete | DELETE | /acl/roles | body | acl-role-delete |
138
+ | acl-grant | POST | /acl/grants | body | acl-grant |
139
+ | acl-revoke | DELETE | /acl/grants | body | acl-revoke |
140
+ | acl-check-gtw | GET | /acl/check | query | acl-can-user-do-gtw |
141
+ | acl-check-resource | GET | /acl/check-resource | query | acl-can-user-do |
142
+ | acl-list-resources-by-user | GET | /acl/resources | query | acl-list-resources-by-user (+ `auth:`) |
143
+
144
+ ```yaml
145
+ gateway:
146
+ mode: gateway
147
+ paths:
148
+ - name: acl-role-upsert # PUT upserts by name. body: { name, actions, description? }
149
+ method: PUT
150
+ path: /acl/roles
151
+ dataSource: body
152
+ topic: rlb-acl
153
+ action: acl-role-update
154
+ mode: rpc
155
+ - name: acl-grant # body: { userId, roles, resourceId?, companyId?, friendlyName? }
156
+ method: POST
157
+ path: /acl/grants
158
+ dataSource: body
159
+ topic: rlb-acl
160
+ action: acl-grant
161
+ mode: rpc
162
+ - name: acl-check-gtw # ?userId=&roles=user&roles=admin → 200 true/false
163
+ method: GET
164
+ path: /acl/check
165
+ dataSource: query
166
+ topic: rlb-acl
167
+ action: acl-can-user-do-gtw
168
+ mode: rpc
169
+ - name: acl-list-resources-by-user # auth-gated; userId from X-GTW-AUTH-USERID
170
+ method: GET
171
+ path: /acl/resources
172
+ dataSource: query
173
+ topic: rlb-acl
174
+ action: acl-list-resources-by-user
175
+ mode: rpc
176
+ auth: gateway-jwks
177
+ ```
178
+
179
+ ## Verify
180
+
181
+ - topic `rlb-acl` + its queue declared on the consuming service; gateway paths use the
182
+ literal `action` strings above.
183
+ - role-gated routes (`roles: [...]`) → `RLB_GTW_ACL_ROLE_SERVICE` bound to an
184
+ `IAclRoleService` (`AclService`). Auth-provider needs `uidClaim` (+ `headerPrefix`).
185
+ - a check returning `false` is a **200**, not an error.
@@ -12,13 +12,17 @@ First, read the shared reference (schema + gotchas):
12
12
  - `.claude/skills/rlb-amqp/references/config-schema.md`
13
13
  - `.claude/skills/rlb-amqp/references/gotchas.md`
14
14
 
15
+ Authoritative source of truth: `docs/broker.md` (decorators + modes) and the runnable
16
+ `sample/config-sample/calculator.ms` (its `src/app.service.ts` + `config/config.yaml`).
17
+
15
18
  Then locate the project's `config.yaml` (commonly `config/config.yaml`) and the service file.
16
19
 
17
20
  ## Inputs to determine (ask only if not inferable)
18
21
 
19
22
  - **topic** (logical name), **action** (string), **mode** intended: `rpc` (default) or `event`.
20
23
  - Payload fields the method needs, and any forwarded headers (e.g. `X-GTW-AUTH-USERID`).
21
- - Whether to also expose it over HTTP (if yes, see the `rlb-amqp-add-route` skill) or WS.
24
+ - Whether to also expose it over HTTP inline via `@BrokerHTTP` (auto-publish to a gateway,
25
+ see Step 1b) or via a `gateway.paths[]` entry (`rlb-amqp-add-route` skill), or over WS.
22
26
 
23
27
  ## Step 1 — the handler method
24
28
 
@@ -31,7 +35,7 @@ import { BrokerAction, BrokerParam } from '@open-rlb/nestjs-amqp';
31
35
 
32
36
  @Injectable()
33
37
  export class <Domain>ActionService {
34
- @BrokerAction('<topic>', '<action>', 'rpc')
38
+ @BrokerAction('<topic>', '<action>', 'rpc') // type? = 'rpc' (default) | 'event'
35
39
  async <method>(
36
40
  @BrokerParam('body', '<field>') field: string,
37
41
  @BrokerParam('header', 'X-GTW-AUTH-USERID') userId: string,
@@ -46,6 +50,85 @@ Ensure the service is a provider in a module that NestJS loads (the
46
50
  `MetadataScannerService` auto-discovers it at boot). Throwing an error whose `name` maps to
47
51
  an HTTP status (e.g. `NotFoundError`) yields the right gateway status.
48
52
 
53
+ ### `@BrokerAction(topic, action, type?)`
54
+
55
+ - `topic` must match a `topics:` entry (an `rpc` topic). `action` is the dispatch key.
56
+ - **`(topic, action)` must be unique** across the whole app — all actions of a topic share
57
+ ONE consumer/queue, dispatched by `action` (gotcha 3).
58
+ - A single method may carry **multiple** `@BrokerAction`s. When it does, any `@BrokerHTTP` on
59
+ that method **must name its `action`** to pair deterministically (decorator order is never
60
+ used). Auth pairs separately — see `@BrokerAuth` below.
61
+
62
+ ### `@BrokerParam(source, name?, pipe?)` — one source per argument
63
+
64
+ No object destructuring; declare a separate param per field. A param with no `@BrokerParam`
65
+ defaults to `source: 'body'` keyed by its own name. Optional `pipe` is a `PipeTransform`.
66
+
67
+ | `source` | Resolves to |
68
+ |---|---|
69
+ | `body` | `payload[name ?? paramName]` — a single body field. |
70
+ | `body-full` | the entire `payload` object. |
71
+ | `header` | `headers[name ?? paramName]` — one AMQP/forwarded header (UPPERCASE+prefixed, gotcha 4). |
72
+ | `tag` | the consumer tag of the delivery. |
73
+ | `action` | the dispatched action string. |
74
+ | `topic` | the topic name. |
75
+
76
+ ## Step 1b (optional) — `@BrokerHTTP` for route auto-publish
77
+
78
+ To expose the same handler over HTTP **without** hand-editing the gateway, stack
79
+ `@BrokerHTTP("METHOD", "/path", dataSource, options?)` on top of the `@BrokerAction`. On boot
80
+ the microservice publishes its `@BrokerHTTP` routes as a manifest to the gateway (route
81
+ auto-discovery), which persists + registers them. This requires `broker.routeDiscovery`
82
+ (see `rlb-amqp` reference / `docs/gateway-admin.md`); the gateway must also declare this
83
+ service's topic so it can forward calls.
84
+
85
+ `dataSource` is `'query' | 'body' | 'params'`. The options object carries
86
+ `{ name?, action?, successStatusCode?, timeout?, parseRaw?, binary?, redirect?, headers?,
87
+ forwardHeaders? }` — it does **not** carry auth.
88
+
89
+ Two independent pairings sit on the method, each only needed in the multi case:
90
+
91
+ - **http ↔ action** (`@BrokerHTTP`'s `action`): bind a route to one of several `@BrokerAction`s.
92
+ Required only when the method declares **more than one** `@BrokerAction`; with a single action
93
+ it defaults to that action.
94
+ - **auth ↔ route** (`@BrokerAuth`'s `httpName` ⇄ `@BrokerHTTP`'s `name`): bind an auth rule to one
95
+ of several routes. Required only when the method declares **more than one** `@BrokerHTTP`; with a
96
+ single route the auth auto-pairs (no `name`/`httpName` needed). A route with no paired
97
+ `@BrokerAuth` is **public**.
98
+
99
+ Auth lives in a separate, decoupled decorator —
100
+ `@BrokerAuth(authName, allowAnonymous?, roles?, httpName?)` — never inside `@BrokerHTTP`'s options.
101
+ This lets two HTTP paths for the SAME action carry DIFFERENT auth.
102
+
103
+ Simple case — one route, auth auto-pairs (no names needed):
104
+
105
+ ```ts
106
+ @BrokerAction('calculator', 'sum')
107
+ @BrokerHTTP('POST', '/calculator/sum', 'body') // dataSource: 'query' | 'body' | 'params'
108
+ @BrokerAuth('cust-jwks', true) // auto-pairs to the single route; public if omitted
109
+ async sum(@BrokerParam('body', 'values') values: number[]) {
110
+ return values.reduce((a, v) => a + v, 0);
111
+ }
112
+ ```
113
+
114
+ Multi case — two routes for ONE action, each name-paired to its own auth:
115
+
116
+ ```ts
117
+ @BrokerAction('booking', 'get-booking')
118
+ @BrokerHTTP('GET', '/bookings/:id', 'params', { name: 'get-booking' })
119
+ @BrokerAuth('cust-jwks', true, undefined, 'get-booking') // httpName ⇄ route name
120
+ @BrokerHTTP('GET', '/admin/bookings/:id', 'params', { name: 'admin-get-booking' })
121
+ @BrokerAuth('admin-jwks', undefined, ['admin'], 'admin-get-booking')
122
+ async getBooking(@BrokerParam('params', 'id') id: string) {
123
+ return this.bookings.find(id);
124
+ }
125
+ ```
126
+
127
+ An `@BrokerAuth` whose `httpName` matches no route is NOT applied and logs a WARNING at
128
+ microservice startup.
129
+
130
+ (Single-route pattern taken verbatim from `sample/config-sample/calculator.ms/src/app.service.ts`.)
131
+
49
132
  ## Step 2 — YAML sync (the critical part)
50
133
 
51
134
  Reconcile `config.yaml` so the topic resolves. Add ONLY what's missing (idempotent):
@@ -85,6 +168,8 @@ single consumer dispatches by `action`.
85
168
  - topic-type exchange ⇒ queue has `routingKey` (gotcha 7)
86
169
  - `(topic, action)` unique (gotcha 3)
87
170
  - header params read the prefixed/uppercased name (gotcha 4)
171
+ - if multiple `@BrokerAction` on one method ⇒ each `@BrokerHTTP` names its `action`
172
+ - if multiple `@BrokerHTTP` on one method ⇒ each `@BrokerAuth` sets `httpName` = the route's `name`
88
173
 
89
174
  ## Step 4 — build
90
175