@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.
- package/README.md +572 -169
- package/amqp-lib/amqp/connection.d.ts +1 -1
- package/common/errors.d.ts +13 -0
- package/common/errors.js +26 -0
- package/common/errors.js.map +1 -0
- package/common/flatten.util.d.ts +1 -0
- package/common/flatten.util.js +29 -0
- package/common/flatten.util.js.map +1 -0
- package/common/index.d.ts +3 -0
- package/common/index.js +20 -0
- package/common/index.js.map +1 -0
- package/common/pagination.model.d.ts +6 -0
- package/common/pagination.model.js +3 -0
- package/common/pagination.model.js.map +1 -0
- package/index.d.ts +3 -0
- package/index.js +3 -0
- package/index.js.map +1 -1
- package/modules/acl/acl.module.d.ts +5 -0
- package/modules/acl/acl.module.js +36 -0
- package/modules/acl/acl.module.js.map +1 -0
- package/modules/acl/cache/acl-cache.service.d.ts +15 -0
- package/modules/acl/cache/acl-cache.service.js +98 -0
- package/modules/acl/cache/acl-cache.service.js.map +1 -0
- package/modules/acl/cache/cache-store.d.ts +6 -0
- package/modules/acl/cache/cache-store.js +3 -0
- package/modules/acl/cache/cache-store.js.map +1 -0
- package/modules/acl/config/acl.config.d.ts +8 -0
- package/modules/acl/config/acl.config.js +3 -0
- package/modules/acl/config/acl.config.js.map +1 -0
- package/modules/acl/const.d.ts +17 -0
- package/modules/acl/const.js +21 -0
- package/modules/acl/const.js.map +1 -0
- package/modules/acl/index.d.ts +11 -0
- package/modules/acl/index.js +28 -0
- package/modules/acl/index.js.map +1 -0
- package/modules/acl/models.d.ts +19 -0
- package/modules/acl/models.js +3 -0
- package/modules/acl/models.js.map +1 -0
- package/modules/acl/repository/acl-action.repository.d.ts +11 -0
- package/modules/acl/repository/acl-action.repository.js +7 -0
- package/modules/acl/repository/acl-action.repository.js.map +1 -0
- package/modules/acl/repository/acl-grant.repository.d.ts +11 -0
- package/modules/acl/repository/acl-grant.repository.js +7 -0
- package/modules/acl/repository/acl-grant.repository.js.map +1 -0
- package/modules/acl/repository/acl-role.repository.d.ts +10 -0
- package/modules/acl/repository/acl-role.repository.js +7 -0
- package/modules/acl/repository/acl-role.repository.js.map +1 -0
- package/modules/acl/services/acl-management.service.d.ts +26 -0
- package/modules/acl/services/acl-management.service.js +202 -0
- package/modules/acl/services/acl-management.service.js.map +1 -0
- package/modules/acl/services/acl.service.d.ts +11 -0
- package/modules/acl/services/acl.service.js +63 -0
- package/modules/acl/services/acl.service.js.map +1 -0
- package/modules/broker/broker.module.d.ts +1 -7
- package/modules/broker/broker.module.js +1 -27
- package/modules/broker/broker.module.js.map +1 -1
- package/modules/broker/services/broker.service.js +2 -2
- package/modules/broker/services/broker.service.js.map +1 -1
- package/modules/gateway-admin/config/gateway-admin.config.d.ts +3 -0
- package/modules/gateway-admin/config/gateway-admin.config.js +3 -0
- package/modules/gateway-admin/config/gateway-admin.config.js.map +1 -0
- package/modules/gateway-admin/const.d.ts +18 -0
- package/modules/gateway-admin/const.js +22 -0
- package/modules/gateway-admin/const.js.map +1 -0
- package/modules/gateway-admin/gateway-admin.module.d.ts +5 -0
- package/modules/gateway-admin/gateway-admin.module.js +35 -0
- package/modules/gateway-admin/gateway-admin.module.js.map +1 -0
- package/modules/gateway-admin/index.d.ts +11 -0
- package/modules/gateway-admin/index.js +28 -0
- package/modules/gateway-admin/index.js.map +1 -0
- package/modules/gateway-admin/models.d.ts +22 -0
- package/modules/gateway-admin/models.js +3 -0
- package/modules/gateway-admin/models.js.map +1 -0
- package/modules/gateway-admin/repository/auth-provider.repository.d.ts +15 -0
- package/modules/gateway-admin/repository/auth-provider.repository.js +7 -0
- package/modules/gateway-admin/repository/auth-provider.repository.js.map +1 -0
- package/modules/gateway-admin/repository/http-metric.repository.d.ts +7 -0
- package/modules/gateway-admin/repository/http-metric.repository.js +7 -0
- package/modules/gateway-admin/repository/http-metric.repository.js.map +1 -0
- package/modules/gateway-admin/repository/http-path.repository.d.ts +15 -0
- package/modules/gateway-admin/repository/http-path.repository.js +7 -0
- package/modules/gateway-admin/repository/http-path.repository.js.map +1 -0
- package/modules/gateway-admin/services/gateway-auth.service.d.ts +14 -0
- package/modules/gateway-admin/services/gateway-auth.service.js +100 -0
- package/modules/gateway-admin/services/gateway-auth.service.js.map +1 -0
- package/modules/gateway-admin/services/gateway-metrics.service.d.ts +11 -0
- package/modules/gateway-admin/services/gateway-metrics.service.js +59 -0
- package/modules/gateway-admin/services/gateway-metrics.service.js.map +1 -0
- package/modules/gateway-admin/services/gateway-path.service.d.ts +14 -0
- package/modules/gateway-admin/services/gateway-path.service.js +106 -0
- package/modules/gateway-admin/services/gateway-path.service.js.map +1 -0
- package/modules/gateway-admin/util/path-order.d.ts +3 -0
- package/modules/gateway-admin/util/path-order.js +36 -0
- package/modules/gateway-admin/util/path-order.js.map +1 -0
- package/modules/proxy/config/path-definition.config.d.ts +14 -1
- package/modules/proxy/proxy.module.d.ts +15 -2
- package/modules/proxy/proxy.module.js +24 -4
- package/modules/proxy/proxy.module.js.map +1 -1
- package/modules/proxy/services/http-auth-handler.service.d.ts +6 -0
- package/modules/proxy/services/http-auth-handler.service.js +48 -24
- package/modules/proxy/services/http-auth-handler.service.js.map +1 -1
- package/modules/proxy/services/http-handler.service.d.ts +5 -1
- package/modules/proxy/services/http-handler.service.js +79 -10
- package/modules/proxy/services/http-handler.service.js.map +1 -1
- package/modules/proxy/services/jwt.service.d.ts +3 -0
- package/modules/proxy/services/jwt.service.js +66 -9
- package/modules/proxy/services/jwt.service.js.map +1 -1
- package/modules/proxy/services/websocket.service.d.ts +33 -6
- package/modules/proxy/services/websocket.service.js +250 -82
- package/modules/proxy/services/websocket.service.js.map +1 -1
- package/package.json +29 -28
- package/schematics/nest-add/files/skills/rlb-amqp/SKILL.md +59 -0
- package/schematics/nest-add/files/skills/rlb-amqp/references/config-schema.md +214 -0
- package/schematics/nest-add/files/skills/rlb-amqp/references/gotchas.md +95 -0
- package/schematics/nest-add/files/skills/rlb-amqp-add-action/SKILL.md +102 -0
- package/schematics/nest-add/files/skills/rlb-amqp-add-route/SKILL.md +61 -0
- package/schematics/nest-add/files/skills/rlb-amqp-add-ws-event/SKILL.md +93 -0
- package/schematics/nest-add/files/skills/rlb-amqp-scaffold/SKILL.md +160 -0
- package/schematics/nest-add/index.js +82 -49
- package/schematics/nest-add/index.js.map +1 -1
- package/schematics/nest-add/index.ts +113 -68
- package/schematics/nest-add/init.schema.d.ts +2 -0
- package/schematics/nest-add/init.schema.ts +11 -1
- package/schematics/nest-add/schema.json +25 -12
- 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.
|