@open-rlb/nestjs-amqp 1.0.27 → 2.0.1

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 (125) hide show
  1. package/README.md +572 -169
  2. package/amqp-lib/amqp/connection.d.ts +1 -1
  3. package/common/errors.d.ts +13 -0
  4. package/common/errors.js +26 -0
  5. package/common/errors.js.map +1 -0
  6. package/common/flatten.util.d.ts +1 -0
  7. package/common/flatten.util.js +29 -0
  8. package/common/flatten.util.js.map +1 -0
  9. package/common/index.d.ts +3 -0
  10. package/common/index.js +20 -0
  11. package/common/index.js.map +1 -0
  12. package/common/pagination.model.d.ts +6 -0
  13. package/common/pagination.model.js +3 -0
  14. package/common/pagination.model.js.map +1 -0
  15. package/index.d.ts +3 -0
  16. package/index.js +3 -0
  17. package/index.js.map +1 -1
  18. package/modules/acl/acl.module.d.ts +5 -0
  19. package/modules/acl/acl.module.js +36 -0
  20. package/modules/acl/acl.module.js.map +1 -0
  21. package/modules/acl/cache/acl-cache.service.d.ts +15 -0
  22. package/modules/acl/cache/acl-cache.service.js +98 -0
  23. package/modules/acl/cache/acl-cache.service.js.map +1 -0
  24. package/modules/acl/cache/cache-store.d.ts +6 -0
  25. package/modules/acl/cache/cache-store.js +3 -0
  26. package/modules/acl/cache/cache-store.js.map +1 -0
  27. package/modules/acl/config/acl.config.d.ts +8 -0
  28. package/modules/acl/config/acl.config.js +3 -0
  29. package/modules/acl/config/acl.config.js.map +1 -0
  30. package/modules/acl/const.d.ts +17 -0
  31. package/modules/acl/const.js +21 -0
  32. package/modules/acl/const.js.map +1 -0
  33. package/modules/acl/index.d.ts +11 -0
  34. package/modules/acl/index.js +28 -0
  35. package/modules/acl/index.js.map +1 -0
  36. package/modules/acl/models.d.ts +19 -0
  37. package/modules/acl/models.js +3 -0
  38. package/modules/acl/models.js.map +1 -0
  39. package/modules/acl/repository/acl-action.repository.d.ts +11 -0
  40. package/modules/acl/repository/acl-action.repository.js +7 -0
  41. package/modules/acl/repository/acl-action.repository.js.map +1 -0
  42. package/modules/acl/repository/acl-grant.repository.d.ts +11 -0
  43. package/modules/acl/repository/acl-grant.repository.js +7 -0
  44. package/modules/acl/repository/acl-grant.repository.js.map +1 -0
  45. package/modules/acl/repository/acl-role.repository.d.ts +10 -0
  46. package/modules/acl/repository/acl-role.repository.js +7 -0
  47. package/modules/acl/repository/acl-role.repository.js.map +1 -0
  48. package/modules/acl/services/acl-management.service.d.ts +26 -0
  49. package/modules/acl/services/acl-management.service.js +202 -0
  50. package/modules/acl/services/acl-management.service.js.map +1 -0
  51. package/modules/acl/services/acl.service.d.ts +11 -0
  52. package/modules/acl/services/acl.service.js +63 -0
  53. package/modules/acl/services/acl.service.js.map +1 -0
  54. package/modules/broker/broker.module.d.ts +1 -7
  55. package/modules/broker/broker.module.js +1 -27
  56. package/modules/broker/broker.module.js.map +1 -1
  57. package/modules/broker/services/broker.service.js +2 -2
  58. package/modules/broker/services/broker.service.js.map +1 -1
  59. package/modules/gateway-admin/config/gateway-admin.config.d.ts +3 -0
  60. package/modules/gateway-admin/config/gateway-admin.config.js +3 -0
  61. package/modules/gateway-admin/config/gateway-admin.config.js.map +1 -0
  62. package/modules/gateway-admin/const.d.ts +18 -0
  63. package/modules/gateway-admin/const.js +22 -0
  64. package/modules/gateway-admin/const.js.map +1 -0
  65. package/modules/gateway-admin/gateway-admin.module.d.ts +5 -0
  66. package/modules/gateway-admin/gateway-admin.module.js +35 -0
  67. package/modules/gateway-admin/gateway-admin.module.js.map +1 -0
  68. package/modules/gateway-admin/index.d.ts +11 -0
  69. package/modules/gateway-admin/index.js +28 -0
  70. package/modules/gateway-admin/index.js.map +1 -0
  71. package/modules/gateway-admin/models.d.ts +22 -0
  72. package/modules/gateway-admin/models.js +3 -0
  73. package/modules/gateway-admin/models.js.map +1 -0
  74. package/modules/gateway-admin/repository/auth-provider.repository.d.ts +15 -0
  75. package/modules/gateway-admin/repository/auth-provider.repository.js +7 -0
  76. package/modules/gateway-admin/repository/auth-provider.repository.js.map +1 -0
  77. package/modules/gateway-admin/repository/http-metric.repository.d.ts +7 -0
  78. package/modules/gateway-admin/repository/http-metric.repository.js +7 -0
  79. package/modules/gateway-admin/repository/http-metric.repository.js.map +1 -0
  80. package/modules/gateway-admin/repository/http-path.repository.d.ts +15 -0
  81. package/modules/gateway-admin/repository/http-path.repository.js +7 -0
  82. package/modules/gateway-admin/repository/http-path.repository.js.map +1 -0
  83. package/modules/gateway-admin/services/gateway-auth.service.d.ts +14 -0
  84. package/modules/gateway-admin/services/gateway-auth.service.js +100 -0
  85. package/modules/gateway-admin/services/gateway-auth.service.js.map +1 -0
  86. package/modules/gateway-admin/services/gateway-metrics.service.d.ts +11 -0
  87. package/modules/gateway-admin/services/gateway-metrics.service.js +59 -0
  88. package/modules/gateway-admin/services/gateway-metrics.service.js.map +1 -0
  89. package/modules/gateway-admin/services/gateway-path.service.d.ts +14 -0
  90. package/modules/gateway-admin/services/gateway-path.service.js +106 -0
  91. package/modules/gateway-admin/services/gateway-path.service.js.map +1 -0
  92. package/modules/gateway-admin/util/path-order.d.ts +3 -0
  93. package/modules/gateway-admin/util/path-order.js +36 -0
  94. package/modules/gateway-admin/util/path-order.js.map +1 -0
  95. package/modules/proxy/config/path-definition.config.d.ts +14 -1
  96. package/modules/proxy/proxy.module.d.ts +15 -2
  97. package/modules/proxy/proxy.module.js +24 -4
  98. package/modules/proxy/proxy.module.js.map +1 -1
  99. package/modules/proxy/services/http-auth-handler.service.d.ts +6 -0
  100. package/modules/proxy/services/http-auth-handler.service.js +48 -24
  101. package/modules/proxy/services/http-auth-handler.service.js.map +1 -1
  102. package/modules/proxy/services/http-handler.service.d.ts +5 -1
  103. package/modules/proxy/services/http-handler.service.js +79 -10
  104. package/modules/proxy/services/http-handler.service.js.map +1 -1
  105. package/modules/proxy/services/jwt.service.d.ts +3 -0
  106. package/modules/proxy/services/jwt.service.js +66 -9
  107. package/modules/proxy/services/jwt.service.js.map +1 -1
  108. package/modules/proxy/services/websocket.service.d.ts +33 -6
  109. package/modules/proxy/services/websocket.service.js +250 -82
  110. package/modules/proxy/services/websocket.service.js.map +1 -1
  111. package/package.json +29 -28
  112. package/schematics/nest-add/files/skills/rlb-amqp/SKILL.md +59 -0
  113. package/schematics/nest-add/files/skills/rlb-amqp/references/config-schema.md +214 -0
  114. package/schematics/nest-add/files/skills/rlb-amqp/references/gotchas.md +95 -0
  115. package/schematics/nest-add/files/skills/rlb-amqp-add-action/SKILL.md +102 -0
  116. package/schematics/nest-add/files/skills/rlb-amqp-add-route/SKILL.md +61 -0
  117. package/schematics/nest-add/files/skills/rlb-amqp-add-ws-event/SKILL.md +93 -0
  118. package/schematics/nest-add/files/skills/rlb-amqp-scaffold/SKILL.md +160 -0
  119. package/schematics/nest-add/index.js +82 -49
  120. package/schematics/nest-add/index.js.map +1 -1
  121. package/schematics/nest-add/index.ts +113 -68
  122. package/schematics/nest-add/init.schema.d.ts +2 -0
  123. package/schematics/nest-add/init.schema.ts +11 -1
  124. package/schematics/nest-add/schema.json +25 -12
  125. package/tsconfig.build.tsbuildinfo +0 -1
@@ -0,0 +1,214 @@
1
+ # config.yaml — full schema
2
+
3
+ Five top-level sections: `app`, `broker`, `topics`, `auth-providers`, `gateway`.
4
+ Loaded by `config/config.loader.ts`. `app`/`broker`/`topics` go to `BrokerModule.forRootAsync`;
5
+ `auth-providers` + `gateway` go to `ProxyModule.forRootAsync` (see the repo `README.md` Quick start).
6
+
7
+ ---
8
+
9
+ ## app
10
+
11
+ ```yaml
12
+ app:
13
+ port: 3000
14
+ host: 0.0.0.0
15
+ environment: development # development | production — controls error detail exposed by the gateway
16
+ ```
17
+
18
+ `AppConfig` = `{ environment, port?, host? }`. In `production` gateway errors are reduced
19
+ to `{ message, name }`; in `development` the full detail/stack is included.
20
+
21
+ ---
22
+
23
+ ## broker (RabbitMQConfig)
24
+
25
+ ```yaml
26
+ broker:
27
+ name: rabbitmq
28
+ uri: "amqp://user:pass@host:5672/vhost" # string | string[] (failover)
29
+ prefetchCount: 10
30
+ defaultRpcTimeout: 10000 # ms
31
+ defaultSubscribeErrorBehavior: ack # ack | reject | requeue
32
+ defaultPublishErrorBehavior: reject
33
+
34
+ connectionManagerOptions: # amqp-connection-manager options
35
+ heartbeatIntervalInSeconds: 60
36
+ reconnectTimeInSeconds: 60
37
+ connectionOptions:
38
+ clientProperties:
39
+ connection_name: my-service # REQUIRED for broadcast + WebSocket gateway
40
+ credentials:
41
+ mechanism: PLAIN # PLAIN | EXTERNAL | AMQPLAIN (case-insensitive)
42
+ username: guest
43
+ password: guest
44
+
45
+ exchanges: # RabbitMQExchangeConfig[]
46
+ - name: users-ex
47
+ type: direct # direct | topic | fanout | headers
48
+ createExchangeIfNotExists: true # false → checkExchange (must pre-exist)
49
+ options: { durable: true, autoDelete: false, internal: false }
50
+
51
+ queues: # RabbitMQQueueConfig[]
52
+ - name: users-rpc-q
53
+ exchange: users-ex
54
+ routingKey: users.rpc # string | string[]; REQUIRED if exchange type == topic
55
+ createQueueIfNotExists: true
56
+ options: { durable: true, exclusive: false, autoDelete: false }
57
+ consumerTag: my-tag # optional, must be unique per channel
58
+
59
+ replyQueues: # map exchange → reply queue (RPC responses)
60
+ users-ex: users-reply-q # omit → RabbitMQ direct-reply-to is used
61
+ ```
62
+
63
+ Notes:
64
+ - `exchanges[]` and `queues[]` are asserted/checked once at boot on the default channel.
65
+ - `replyQueues` values are auto-consumed at boot.
66
+ - Queue `options` is amqplib `Options.AssertQueue` (durable, exclusive, autoDelete,
67
+ messageTtl, deadLetterExchange, maxLength, maxPriority, arguments, ...).
68
+
69
+ ---
70
+
71
+ ## topics (BrokerTopic[])
72
+
73
+ A topic maps a logical name to an AMQP path. `mode` decides the semantics.
74
+
75
+ ```yaml
76
+ topics:
77
+ - name: users-rpc # logical name (must match @BrokerAction / requestData / gateway)
78
+ mode: rpc # rpc | handle | broadcast | event
79
+ queue: users-rpc-q # for rpc/handle: must exist in broker.queues[]
80
+ exchange: users-ex # for broadcast/event (direct exchange path)
81
+ routingKey: users.rpc # for broadcast/event / topic exchanges
82
+ toObservable: false # handle only: route to BrokerService.events$ instead of a handler
83
+ ```
84
+
85
+ | mode | required fields | notes |
86
+ | ----------- | ------------------------------------------------ | -------------------------------------- |
87
+ | `rpc` | `name`, `queue` (or `exchange`+`routingKey`) | request/response + timeout |
88
+ | `handle` | `name`, `queue` | simple queue worker |
89
+ | `broadcast` | `name`, `exchange`, `routingKey` | fanout/topic; needs `connection_name` |
90
+ | `event` | `name`, `queue` OR `exchange`+`routingKey` | fire-and-forget |
91
+
92
+ > A single `@BrokerAction` topic registers ONE consumer; multiple actions on the same
93
+ > topic share it and are dispatched by `action`.
94
+
95
+ ---
96
+
97
+ ## auth-providers (HandlerAuthConfig[])
98
+
99
+ ```yaml
100
+ auth-providers:
101
+ - name: gateway-jwks
102
+ type: jwks # jwt | jwks | basic | str-compare | none
103
+ issuer: https://issuer/realms/x
104
+ jwksUri: https://issuer/certs # jwks only
105
+ secret: s3cr3t # jwt / str-compare only
106
+ audience: my-aud # jwt only
107
+ algorithms: [RS256]
108
+ httpsAllowUnauthorized: false # true ONLY for self-signed dev issuers
109
+ clientId: u # basic only
110
+ clientSecret: p # basic only
111
+ jwtMap: [sub:userId, roles:roles]# tokenClaim:destClaim (dest is header-prefixed + uppercased)
112
+ headerPrefix: X-GTW-AUTH- # prefix of headers propagated to microservices
113
+ uidClaim: USERID # dest used as user id for ACL
114
+ usernameClaim: USERNAME
115
+ aclTopic: acl # RPC topic queried for roles
116
+ aclAction: can-user-do
117
+ ```
118
+
119
+ Mapping example: token `{ sub: "u_1" }` + `jwtMap: [sub:userId]` + `headerPrefix: X-GTW-AUTH-`
120
+ → header `X-GTW-AUTH-USERID = u_1`. Read it in a handler with
121
+ `@BrokerParam('header', 'X-GTW-AUTH-USERID')`.
122
+
123
+ Types: `jwt` (HS/RS secret), `jwks` (remote keys), `basic` (clientId/clientSecret),
124
+ `str-compare` (static token after `headerPrefix` in Authorization).
125
+
126
+ Provider notes: `algorithms` is REQUIRED for `jwt`/`jwks` (omit → denied; `jwks` allows only
127
+ RS*/ES*/PS*, rejects HS*/none). `str-compare` without `secret` and `basic` without
128
+ `clientSecret` PASS THROUGH (request treated as authenticated — provider effectively open).
129
+ Define `jwtMap` to avoid forwarding unmapped claims.
130
+
131
+ ---
132
+
133
+ ## gateway (GatewayConfig)
134
+
135
+ ```yaml
136
+ gateway:
137
+ mode: gateway
138
+ headerPrefix: X-GTW- # prefix for forwarded request headers (forwardHeaders)
139
+
140
+ ws: # WebSocketGatewayOptions — connection-level only
141
+ maxConnections: 5000
142
+ maxSubscriptionsPerClient: 50
143
+ heartbeatIntervalMs: 30000
144
+ allowedOrigins: [https://app.example.com] # Origin allowlist (omit → all accepted)
145
+ maxMessageBytes: 16384 # drop oversized client frames (default 16KB)
146
+ # auth/roles/scope are declared PER-EVENT on events[], not here
147
+
148
+ loadConfig: # optional remote load via RPC at boot
149
+ paths: { topic: gtw.config, action: get-paths }
150
+ events: { topic: gtw.config, action: get-events }
151
+
152
+ paths: [ ... ] # PathDefinition[] (HTTP routes) — see below
153
+ events: [ ... ] # WebSocketEvent[] (WS / webhook) — see below
154
+ ```
155
+
156
+ ### gateway.paths[] (PathDefinition — HTTP routes)
157
+
158
+ ```yaml
159
+ - name: users-create
160
+ method: POST # GET | POST | PUT | DELETE | PATCH
161
+ path: /users/:tenant? # Express route, params supported
162
+ dataSource: body # body | query | params | body-query | query-body
163
+ topic: users-rpc
164
+ action: user.create
165
+ mode: rpc # rpc | event
166
+ timeout: 7000 # rpc only (ms)
167
+ auth: gateway-jwks # auth-provider name
168
+ allowAnonymous: false # true → allow even without valid auth
169
+ roles: [users.create] # requires IAclRoleService
170
+ successStatusCode: 201
171
+ binary: false # true → response sent as base64-decoded Buffer
172
+ redirect: 302 # if set → redirect to the URL contained in the reply
173
+ parseRaw: false # true → forward raw body as $raw (needs rawBody:true in bootstrap)
174
+ headers: { Cache-Control: no-store } # static response headers
175
+ forwardHeaders: { Tenant: x-tenant } # request header → forwarded to the microservice
176
+ ```
177
+
178
+ dataSource payload composition:
179
+
180
+ | value | payload |
181
+ | ------------ | -------------------------------- |
182
+ | `body` | `{...params, ...body}` |
183
+ | `query` | `{...params, ...query}` |
184
+ | `params` | `params` |
185
+ | `body-query` | `{...params, ...query, ...body}` |
186
+ | `query-body` | `{...params, ...body, ...query}` |
187
+
188
+ Route params are re-applied last (win on key collisions). Uploads → `$files`, raw → `$raw`.
189
+ Error `name` → HTTP status: BadRequestError/InvalidParamsErrror→400, UnauthorizedError→401,
190
+ ForbiddenError→403, NotFoundError→404, else→500. `mode: event` confirm failure → 503.
191
+
192
+ ### gateway.events[] (WebSocketEvent — WS / webhook)
193
+
194
+ ```yaml
195
+ - name: orders
196
+ type: ws # ws | http (webhook)
197
+ exchange: orders-ex
198
+ routingKey: orders.#
199
+ auth: gateway-jwks # provider that verifies the token + maps claims FOR THIS event (at subscribe)
200
+ requireAuth: true # default true when `auth` is set; false → auth optional (anon allowed)
201
+ roles: [orders.read] # ACL check via IAclRoleService
202
+ scopeClaim: X-GTW-AUTH-USERID # forward only messages of this user...
203
+ payloadKey: userId # ...where payload.userId === the mapped claim value
204
+ # type: http only:
205
+ url: https://hooks.example.com/orders
206
+ method: POST
207
+ timeout: 8000
208
+ headers: { Authorization: "Bearer ..." }
209
+ ```
210
+
211
+ WS client connects with the JWT in the subprotocol: `new WebSocket(url, [token])`. The token
212
+ is verified per-event with `events[].auth`'s provider (memoized per provider per connection).
213
+ Client protocol: `{action:'subscribe'|'unsubscribe', topic, select?}`; inbound messages
214
+ arrive as `{ topic: 'on<Name>', data }`, errors as `{ topic:'onError', data:{event,error} }`.
@@ -0,0 +1,95 @@
1
+ # Gotchas — bug-prone cases checklist
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.
5
+
6
+ ## Decorators & handlers
7
+ 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.
10
+ 2. **Avoid default parameter values.** Only a basic `= value` strip exists
11
+ (`removeDefaultsFromParams`); complex defaults misalign mapping. Always pass an explicit
12
+ `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.
16
+ 4. **Forwarded headers are UPPERCASE + prefixed.** Read `@BrokerParam('header',
17
+ 'X-GTW-AUTH-USERID')`, not `'userId'`.
18
+
19
+ ## Topic ↔ queue ↔ exchange wiring
20
+ 5. **The topic `name` must match everywhere**: `@BrokerAction`, `topics[].name`,
21
+ `requestData`/`publishMessage`, `gateway.paths[].topic` / `events[]`. Typo →
22
+ `Topic X not found in configuration`.
23
+ 6. **`mode: rpc`/`handle` need `topics[].queue` in `broker.queues[]`**, and that queue's
24
+ `exchange` in `broker.exchanges[]`. In `handle` a missing queue throws NPE at boot
25
+ (`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.
30
+
31
+ ## 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.
39
+
40
+ ## 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.
47
+
48
+ ## Auth / ACL
49
+ 15. **`roles` (HTTP path or WS event) require an `IAclRoleService`** registered via
50
+ `RLB_GTW_ACL_ROLE_SERVICE` in `ProxyModule.forRootAsync({ providers: [...] })`. The
51
+ provider must define `aclTopic`, `aclAction`, `uidClaim`, `usernameClaim`, and
52
+ `uidClaim` must match a `jwtMap` dest. Missing → throw.
53
+ 16. **Auth-providers + gateway config are passed to `ProxyModule`** (`authOptions` /
54
+ `gatewayOptions`), not `BrokerModule`. `BrokerModule` owns only `options`/`topics`/`appOptions`.
55
+
56
+ ## WebSocket
57
+ 16. **Auth is per-event, not global.** `events[].auth` names the provider that verifies the
58
+ connection token AND maps its claims for THAT event (at subscribe time, memoized per
59
+ provider). `scopeClaim` references the MAPPED claim (with `headerPrefix`, e.g.
60
+ `X-GTW-AUTH-USERID`), not the raw token claim. `payloadKey` is the event-payload field.
61
+ `scopeClaim` without `payloadKey` denies everything (safe default). `requireAuth: false`
62
+ on an event makes `auth` optional (anon allowed, claims mapped if a token is present).
63
+ `gateway.ws` only carries connection-level limits/heartbeat — no auth fields.
64
+ 17. **Don't use a fixed durable queue for WS events.** The lib creates a per-instance
65
+ exclusive ephemeral queue for fan-out; a shared queue makes instances compete and clients
66
+ on one instance miss messages.
67
+ 18. **Token transport is the subprotocol**: `new WebSocket(url, [token])`. Browsers can't set
68
+ custom handshake headers.
69
+
70
+ ## Publish / event
71
+ 19. **`publishMessage` is `async` — `await` it** for the publisher-confirm guarantee and to
72
+ catch failures. Un-awaited = fire-and-forget without guarantee.
73
+ 20. **`handle`/`broadcast` handlers must return `void`**; a return value logs
74
+ `Subscribe handlers should only return void`.
75
+
76
+ ## TLS / credentials / provider hardening
77
+ 21. **JWKS verifies TLS by default.** `httpsAllowUnauthorized: true` only for self-signed dev
78
+ issuers.
79
+ 22. **Credential `mechanism`**: `PLAIN` | `EXTERNAL` | `AMQPLAIN` (case-insensitive). Unknown
80
+ value leaves `response` unset → auth fails.
81
+ 23. **`algorithms` is REQUIRED for `jwt`/`jwks`.** If omitted, verification is denied
82
+ (algorithm-confusion guard). For `jwks` only asymmetric algs are allowed (RS*/ES*/PS*);
83
+ `HS*`/`none` are rejected.
84
+ 24. **`str-compare`/`basic` PASS THROUGH when their secret is unset.** A `str-compare`
85
+ without `secret` or a `basic` without `clientSecret` treats every request as authenticated
86
+ (provider effectively open/disabled — by design). Set the secret to actually enforce it.
87
+ 25. **Define `jwtMap`.** Without it, every token claim is forwarded unmapped (over-exposure).
88
+
89
+ ## WebSocket session/transport security
90
+ 26. **WS sessions are bounded by the token `exp`.** The connection is closed (`1008`) when the
91
+ JWT expires; no delivery happens afterward. Long-lived sockets need token refresh +
92
+ reconnect.
93
+ 27. **Set `gateway.ws.allowedOrigins`** to reject cross-site handshakes; if omitted, all
94
+ Origins are accepted (logged at boot). `maxMessageBytes` (default 16384) drops oversized
95
+ client frames.
@@ -0,0 +1,102 @@
1
+ ---
2
+ name: rlb-amqp-add-action
3
+ description: Add or modify a @BrokerAction handler in a @open-rlb/nestjs-amqp microservice AND keep config.yaml in sync. Use whenever the user adds/changes a broker action, RPC method, or event handler, or says the YAML must be updated to match a new @BrokerAction method (topic/queue/exchange, and optionally a gateway route). Generates the decorated method and the exact YAML fragments to add.
4
+ ---
5
+
6
+ # Add a @BrokerAction handler and sync config.yaml
7
+
8
+ Goal: when a broker handler is added or changed, produce BOTH the decorated method and the
9
+ matching `config.yaml` fragments, with no missing wiring.
10
+
11
+ First, read the shared reference (schema + gotchas):
12
+ - `.claude/skills/rlb-amqp/references/config-schema.md`
13
+ - `.claude/skills/rlb-amqp/references/gotchas.md`
14
+
15
+ Then locate the project's `config.yaml` (commonly `config/config.yaml`) and the service file.
16
+
17
+ ## Inputs to determine (ask only if not inferable)
18
+
19
+ - **topic** (logical name), **action** (string), **mode** intended: `rpc` (default) or `event`.
20
+ - 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.
22
+
23
+ ## Step 1 — the handler method
24
+
25
+ Add an `@Injectable()` service method. Keep parameters FLAT (no destructuring, no default
26
+ values — see gotchas 1–2). Always pass an explicit `name` to `@BrokerParam`.
27
+
28
+ ```ts
29
+ import { Injectable } from '@nestjs/common';
30
+ import { BrokerAction, BrokerParam } from '@open-rlb/nestjs-amqp';
31
+
32
+ @Injectable()
33
+ export class <Domain>ActionService {
34
+ @BrokerAction('<topic>', '<action>', 'rpc')
35
+ async <method>(
36
+ @BrokerParam('body', '<field>') field: string,
37
+ @BrokerParam('header', 'X-GTW-AUTH-USERID') userId: string,
38
+ ) {
39
+ // rpc → return a value; event callers ignore it.
40
+ return { ok: true };
41
+ }
42
+ }
43
+ ```
44
+
45
+ Ensure the service is a provider in a module that NestJS loads (the
46
+ `MetadataScannerService` auto-discovers it at boot). Throwing an error whose `name` maps to
47
+ an HTTP status (e.g. `NotFoundError`) yields the right gateway status.
48
+
49
+ ## Step 2 — YAML sync (the critical part)
50
+
51
+ Reconcile `config.yaml` so the topic resolves. Add ONLY what's missing (idempotent):
52
+
53
+ 1. **Topic** in `topics[]`:
54
+ ```yaml
55
+ - name: <topic>
56
+ mode: rpc # or handle/event
57
+ queue: <topic>-q # rpc/handle: must reference a queue below
58
+ ```
59
+ 2. **Queue** in `broker.queues[]` (for rpc/handle):
60
+ ```yaml
61
+ - name: <topic>-q
62
+ exchange: <exchange>
63
+ routingKey: <topic> # REQUIRED if the exchange is type: topic
64
+ createQueueIfNotExists: true
65
+ options: { durable: true }
66
+ ```
67
+ 3. **Exchange** in `broker.exchanges[]` (if not present):
68
+ ```yaml
69
+ - name: <exchange>
70
+ type: direct # or topic — then queue.routingKey is mandatory
71
+ createExchangeIfNotExists: true
72
+ options: { durable: true }
73
+ ```
74
+ 4. For an **event-publishable** topic (no reply) you can instead use
75
+ `exchange` + `routingKey` directly on the topic.
76
+
77
+ If the action is just another action on an EXISTING topic, do NOT add a new
78
+ queue/exchange — only ensure the `(topic, action)` pair is unique (gotcha 3). The topic's
79
+ single consumer dispatches by `action`.
80
+
81
+ ## Step 3 — verify against gotchas
82
+
83
+ - topic `name` identical in code and YAML (gotcha 5)
84
+ - queue exists and its exchange exists (gotcha 6)
85
+ - topic-type exchange ⇒ queue has `routingKey` (gotcha 7)
86
+ - `(topic, action)` unique (gotcha 3)
87
+ - header params read the prefixed/uppercased name (gotcha 4)
88
+
89
+ ## Step 4 — build
90
+
91
+ Run `npm run build`. Optionally show the user the RPC vs event call sites:
92
+
93
+ ```ts
94
+ await broker.requestData('<topic>', '<action>', payload, headers); // waits reply
95
+ await broker.publishMessage('<topic>', '<action>', payload, headers); // awaits confirm
96
+ ```
97
+
98
+ ## Output
99
+
100
+ Present: (a) the handler diff, (b) the exact YAML fragments to insert (with their parent
101
+ path), (c) a one-line note of any gotcha you had to satisfy. If multiple files/fragments,
102
+ list them so the user can review before applying.
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: rlb-amqp-add-route
3
+ description: Expose a broker action over HTTP through the @open-rlb/nestjs-amqp gateway by adding a gateway.paths[] entry. Use when the user wants a new HTTP endpoint/REST route that forwards to a topic/action, choosing rpc (wait reply) vs event (fire-and-forget with confirm), with auth, roles, dataSource, timeout, file upload or raw body. Generates the YAML path fragment and flags required bootstrap/ACL wiring.
4
+ ---
5
+
6
+ # Add an HTTP gateway route (gateway.paths[])
7
+
8
+ Read first:
9
+ - `.claude/skills/rlb-amqp/references/config-schema.md` (the `gateway.paths[]` section)
10
+ - `.claude/skills/rlb-amqp/references/gotchas.md` (HTTP + auth items 11–15)
11
+
12
+ The target `topic`+`action` should already have a handler (otherwise also run
13
+ `rlb-amqp-add-action`). The route only needs the topic to exist in `topics[]`.
14
+
15
+ ## Decide
16
+
17
+ - **mode**: `rpc` (return the handler's response) or `event` (202 after publisher confirm,
18
+ 503 on failure).
19
+ - **dataSource**: how to build the payload — `body` | `query` | `params` | `body-query` |
20
+ `query-body` (see the composition table in the schema).
21
+ - **auth**: an `auth-provider` name; `allowAnonymous: true` to permit unauthenticated access;
22
+ `roles: [...]` for ACL.
23
+ - Extras: `timeout` (rpc), `successStatusCode`, `binary`, `redirect`, `parseRaw`, static
24
+ `headers`, `forwardHeaders`.
25
+
26
+ ## YAML fragment
27
+
28
+ ```yaml
29
+ gateway:
30
+ paths:
31
+ - name: <unique-name>
32
+ method: POST # GET | POST | PUT | DELETE | PATCH
33
+ path: /resource/:id?
34
+ dataSource: body
35
+ topic: <topic>
36
+ action: <action>
37
+ mode: rpc # or event
38
+ auth: gateway-jwks # optional
39
+ roles: [resource.write] # optional → needs IAclRoleService
40
+ timeout: 7000 # rpc only
41
+ successStatusCode: 201
42
+ ```
43
+
44
+ ## Required wiring to flag
45
+
46
+ - If `parseRaw: true` → the app must bootstrap with
47
+ `NestFactory.create(AppModule, { rawBody: true })` (gotcha 12).
48
+ - If `roles` is used → an `IAclRoleService` must be registered via
49
+ `RLB_GTW_ACL_ROLE_SERVICE` in `ProxyModule.forRootAsync({ providers: [...] })`, and the
50
+ auth-provider must define `aclTopic`/`aclAction`/`uidClaim`/`usernameClaim` (gotcha 15).
51
+ - Forwarded auth claims reach the handler as prefixed/uppercased headers
52
+ (e.g. `X-GTW-AUTH-USERID`) — read them with `@BrokerParam('header', ...)`.
53
+
54
+ ## Verify
55
+
56
+ - topic exists in `topics[]` and resolves (gotchas 5–7).
57
+ - route-param vs body/query key collisions are intentional (gotcha 13).
58
+ - `npm run build`, then optionally curl the route once the broker is up.
59
+
60
+ Output the YAML fragment (with parent path), plus any bootstrap/ACL action the user still
61
+ needs to take.
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: rlb-amqp-add-ws-event
3
+ description: Add a secure WebSocket event (or HTTP webhook) to the @open-rlb/nestjs-amqp gateway by adding a gateway.events[] entry. Use when the user wants to push broker messages to connected WebSocket clients or to a webhook, with authentication (token in subprotocol), per-event roles/ACL, and per-user scoping to avoid leaking other users' data. Generates the YAML event fragment plus the exchange/queue and ws options, and flags the security wiring.
4
+ ---
5
+
6
+ # Add a WebSocket / webhook event (gateway.events[])
7
+
8
+ Read first:
9
+ - `.claude/skills/rlb-amqp/references/config-schema.md` (the `gateway.events[]` + `gateway.ws`
10
+ sections)
11
+ - `.claude/skills/rlb-amqp/references/gotchas.md` (WebSocket items 16–18, ACL item 15)
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.
15
+
16
+ ## Decide
17
+
18
+ - **type**: `ws` (push to clients) or `http` (forward each message to a webhook `url`).
19
+ - **source**: `exchange` + `routingKey` (the exchange must exist in `broker.exchanges[]`;
20
+ `connection_name` must be set — gotcha 8).
21
+ - **security**:
22
+ - `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.
24
+ - `requireAuth: false` → makes `auth` optional (anonymous allowed; claims mapped if a token
25
+ is present — handy with `scopeClaim`). Defaults to `true` when `auth` is set.
26
+ - `roles: [...]` → ACL check (needs `IAclRoleService`); requires `auth` for the identity.
27
+ - `scopeClaim` + `payloadKey` → per-user scoping: a client only receives messages where
28
+ `payload[payloadKey] === claims[scopeClaim]`. `scopeClaim` is the MAPPED claim
29
+ (with `headerPrefix`, e.g. `X-GTW-AUTH-USERID`). Without `payloadKey` it denies all
30
+ (gotcha 16).
31
+
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.
34
+
35
+ ## YAML fragments
36
+
37
+ ```yaml
38
+ gateway:
39
+ ws: # connection-level only (optional)
40
+ maxConnections: 5000
41
+ maxSubscriptionsPerClient: 50
42
+ heartbeatIntervalMs: 30000
43
+
44
+ events:
45
+ - name: orders
46
+ type: ws
47
+ exchange: orders-ex # must exist in broker.exchanges[]
48
+ routingKey: orders.#
49
+ auth: gateway-jwks # verifies token + maps claims for this event
50
+ requireAuth: true # default true when auth is set; false → optional
51
+ roles: [orders.read] # optional → needs IAclRoleService
52
+ scopeClaim: X-GTW-AUTH-USERID # optional per-user scoping
53
+ payloadKey: userId
54
+
55
+ - name: invoices # webhook variant
56
+ type: http
57
+ exchange: inv-ex
58
+ routingKey: inv.#
59
+ url: https://hooks.example.com/invoices
60
+ method: POST
61
+ timeout: 8000
62
+ ```
63
+
64
+ Ensure the exchange exists:
65
+
66
+ ```yaml
67
+ broker:
68
+ exchanges:
69
+ - name: orders-ex
70
+ type: topic
71
+ createExchangeIfNotExists: true
72
+ options: { durable: true }
73
+ ```
74
+
75
+ ## Required wiring to flag
76
+
77
+ - The app bootstrap must register the WS adapter:
78
+ `app.useWebSocketAdapter(new WsAdapter(app))`.
79
+ - `events[].auth` must reference a `jwt`/`jwks` provider; subscribing without a valid token
80
+ yields `{ topic:'onError', data:{ event, error:'unauthorized' } }` (unless `requireAuth:false`).
81
+ - `roles` → `IAclRoleService` via `RLB_GTW_ACL_ROLE_SERVICE` (gotcha 15).
82
+ - Do NOT add a fixed durable queue for the event — the lib creates a per-instance exclusive
83
+ queue for fan-out (gotcha 17).
84
+
85
+ ## Client snippet (for docs/testing)
86
+
87
+ ```js
88
+ const ws = new WebSocket('ws://localhost:3000', [token]); // token in subprotocol
89
+ ws.onopen = () => ws.send(JSON.stringify({ action: 'subscribe', topic: 'orders' }));
90
+ ws.onmessage = (e) => console.log(JSON.parse(e.data)); // { topic:'onOrders', data } | { topic:'onError', ... }
91
+ ```
92
+
93
+ Output the YAML fragments (with parent paths) and the bootstrap/ACL items still required.