@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
package/README.md CHANGED
@@ -1,115 +1,331 @@
1
1
  # @open-rlb/nestjs-amqp
2
2
 
3
- Quick guide for using the npm package.
3
+ Libreria **NestJS** che fornisce un'astrazione di alto livello su **RabbitMQ/AMQP**, più un **API Gateway HTTP/WebSocket** che traduce le richieste esterne in messaggi sul broker.
4
4
 
5
- Guide scope: `rpc`, `handle`, `broadcast`.
6
-
7
- ## Installation
5
+ È il cuore di un'architettura a microservizi event-driven: i servizi comunicano tra loro via RabbitMQ con semplici decoratori, e un gateway espone tutto al mondo esterno via HTTP/WS, il tutto guidato dalla configurazione YAML.
8
6
 
9
7
  ```bash
10
8
  npm i @open-rlb/nestjs-amqp
11
9
  ```
12
10
 
13
- ## Basic setup
11
+ ### Installazione automatica (`nest add`)
12
+
13
+ Uno **schematic** wira la libreria nel tuo progetto NestJS: aggiunge i moduli all'`AppModule`, crea il config loader e un `config.yaml`, copia le **skill Claude** in `.claude/skills/` e — in base alla modalità gateway — include o meno la parte HTTP/WebSocket (sia nello YAML sia nella factory dei moduli).
14
+
15
+ ```bash
16
+ # con gateway HTTP/WebSocket (default)
17
+ nest add @open-rlb/nestjs-amqp
18
+
19
+ # solo microservizio AMQP (niente gateway)
20
+ nest g @open-rlb/nestjs-amqp:nest-add --gateway=false
21
+ ```
22
+
23
+ Opzioni: `--gateway` (on/off, default on), `--module` (default `src/app.module.ts`), `--main` (default `src/main.ts`), `--config` (default `config/config.yaml`), `--skills` (copia le skill, default on), `--skip-install`.
24
+
25
+ Con `--gateway=false` la factory passa a `BrokerModule` solo `{ options, topics, appOptions }` e non importa `ProxyModule`/`HttpModule`; con il gateway attivo aggiunge `ProxyModule.forRootAsync(...)` (che riceve `authOptions` + `gatewayOptions`), `HttpModule` e il `WsAdapter` in `main.ts`. Lo schematic è idempotente (non tocca un `AppModule` che già importa `BrokerModule`).
26
+
27
+ > Documentazione completa. Indice:
28
+ > [Architettura](#architettura) ·
29
+ > [Quick start](#quick-start) ·
30
+ > [Configurazione](#configurazione-completa) ·
31
+ > [Scrivere un microservizio (AMQP)](#scrivere-un-microservizio-amqp) ·
32
+ > [Gateway HTTP](#gateway-http) ·
33
+ > [Gateway WebSocket](#gateway-websocket) ·
34
+ > [Remote config](#remote-config) ·
35
+ > [API `BrokerService`](#api-brokerservice) ·
36
+ > [⚠️ Gotcha e casi a rischio bug](#️-gotcha-e-casi-a-rischio-bug) ·
37
+ > [Errori comuni](#errori-comuni)
38
+
39
+ ---
40
+
41
+ ## Architettura
42
+
43
+ Monorepo NestJS (vedi `nest-cli.json`):
44
+
45
+ | Progetto | Tipo | Descrizione |
46
+ | ------------------------ | ----------- | ------------------------------------------------ |
47
+ | `libs/rlb-nestjs-amqp` | library | La libreria vera e propria (il prodotto npm) |
48
+ | `apps/gateway` | application | App di esempio/riferimento che usa la libreria |
49
+
50
+ ```
51
+ ┌─────────────────────────────────────────────────────────┐
52
+ │ Client esterni (HTTP, WebSocket) │
53
+ └───────────────┬─────────────────────────────────────────┘
54
+
55
+ ┌───────▼────────┐ modules/proxy ── Gateway
56
+ │ HttpHandler │ - registra route HTTP dinamiche
57
+ │ WebSocketSvc │ - auth (jwt/jwks/basic/str-compare) + ACL/ruoli
58
+ │ JwtService │ - traduce HTTP/WS → messaggi broker
59
+ └───────┬────────┘
60
+
61
+ ┌───────▼────────┐ modules/broker ── Astrazione AMQP
62
+ │ BrokerService │ - rpc / handle / broadcast / event
63
+ │ MetadataScanner│ - decoratori @BrokerAction / @BrokerParam
64
+ │ HandlerRegistry│ - auto-discovery dei metodi via reflect-metadata
65
+ └───────┬────────┘
66
+
67
+ ┌───────▼────────┐ amqp-lib ── Driver AMQP a basso livello
68
+ │ AmqpConnection │ - connessione gestita (riconnessione, canali)
69
+ │ │ - publish/consume, RPC con correlationId, Nack
70
+ └───────┬────────┘
71
+
72
+ ┌─────▼─────┐
73
+ │ RabbitMQ │
74
+ └───────────┘
75
+ ```
76
+
77
+ ### I tre strati
78
+
79
+ 1. **`amqp-lib`** — driver a basso livello (`AmqpConnection`): connessione resiliente (`amqp-connection-manager`), canali gestiti, setup di exchange/queue/binding al boot, RPC con `correlationId` + *direct-reply-to*, consumer con gestione errori (`Nack` → ack/reject/requeue), graceful shutdown.
80
+ 2. **`modules/broker`** — astrazione di business: `BrokerService`, decoratori `@BrokerAction`/`@BrokerParam`, `MetadataScannerService` (auto-discovery dei metodi decorati e registrazione automatica dei consumer).
81
+ 3. **`modules/proxy`** — gateway HTTP/WebSocket: registrazione dinamica di route Express, auth pluggable, ACL/ruoli, WebSocket sicuro e scalabile, forwarding webhook.
82
+
83
+ ### Flusso di una richiesta
84
+
85
+ ```
86
+ HTTP/WS request → Gateway → (RPC | event) su RabbitMQ → microservizio (@BrokerAction)
87
+ → risposta (solo RPC) → HTTP/WS response
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Quick start
14
93
 
15
- ### `AppModule`
94
+ ### 1. `AppModule`
16
95
 
17
96
  ```ts
18
97
  import { HttpModule } from '@nestjs/axios';
19
98
  import { Module } from '@nestjs/common';
20
99
  import { ConfigModule, ConfigService } from '@nestjs/config';
21
- import { BrokerModule, ProxyModule } from '@open-rlb/nestjs-amqp';
100
+ import { AppConfig, BrokerModule, BrokerTopic, GatewayConfig, ProxyModule } from '@open-rlb/nestjs-amqp';
101
+ import { RabbitMQConfig } from '@open-rlb/nestjs-amqp/amqp-lib/config/rabbitmq.config';
102
+ import { HandlerAuthConfig } from '@open-rlb/nestjs-amqp/modules/broker/config/handler-auth.config';
103
+ import yamlConfig from './config/config.loader';
22
104
 
23
105
  @Module({
24
106
  imports: [
107
+ ConfigModule.forRoot({ isGlobal: true, load: [yamlConfig] }),
25
108
  BrokerModule.forRootAsync({
26
109
  imports: [ConfigModule],
27
110
  inject: [ConfigService],
28
111
  useFactory: async (config: ConfigService) => ({
29
- options: config.get('broker'),
30
- topics: config.get('topics'),
31
- appOptions: config.get('app'),
32
- authOptions: config.get('auth-providers'),
33
- gatewayOptions: config.get('gateway'),
112
+ options: config.get<RabbitMQConfig>('broker'),
113
+ topics: config.get<BrokerTopic[]>('topics'),
114
+ appOptions: config.get<AppConfig>('app'),
34
115
  }),
35
116
  }),
36
117
  HttpModule,
37
- ProxyModule.forRoot([]),
118
+ // auth-providers + gateway config → ProxyModule (non più BrokerModule)
119
+ ProxyModule.forRootAsync({
120
+ imports: [ConfigModule],
121
+ inject: [ConfigService],
122
+ useFactory: (config: ConfigService) => ({
123
+ authOptions: config.get<HandlerAuthConfig[]>('auth-providers'),
124
+ gatewayOptions: config.get<GatewayConfig>('gateway'),
125
+ }),
126
+ providers: [
127
+ // { provide: RLB_GTW_ACL_ROLE_SERVICE, useClass: MyAclService }, // solo se usi `roles`
128
+ ],
129
+ }),
38
130
  ],
39
131
  })
40
132
  export class AppModule {}
41
133
  ```
42
134
 
43
- ### Bootstrap
135
+ ### 2. Bootstrap (`main.ts`)
136
+
137
+ ```ts
138
+ import { NestFactory } from '@nestjs/core';
139
+ import { WsAdapter } from '@nestjs/platform-ws';
140
+ import { AppModule } from './app.module';
141
+
142
+ async function bootstrap() {
143
+ // rawBody: true è OBBLIGATORIO se usi parseRaw nelle route del gateway
144
+ const app = await NestFactory.create(AppModule, { rawBody: true });
145
+ app.useWebSocketAdapter(new WsAdapter(app)); // solo se usi il gateway WebSocket
146
+ await app.listen(3000, '0.0.0.0');
147
+ }
148
+ bootstrap();
149
+ ```
44
150
 
45
- If you use `parseRaw: true` in gateway routes:
151
+ ### 3. Config loader (`config/config.loader.ts`)
46
152
 
47
153
  ```ts
48
- const app = await NestFactory.create(AppModule, { rawBody: true });
154
+ import { readFileSync } from 'fs';
155
+ import * as yaml from 'js-yaml';
156
+ import { join } from 'path';
157
+
158
+ const YAML_CONFIG_FILENAME = 'config/config.yaml';
159
+
160
+ export default () =>
161
+ yaml.load(readFileSync(join(process.cwd(), YAML_CONFIG_FILENAME), 'utf8')) as Record<string, any>;
49
162
  ```
50
163
 
51
- ## Minimal `config.yaml`
164
+ ---
52
165
 
53
- ```yaml
54
- # yaml-language-server: $schema=./schema.json
166
+ ## Configurazione completa
167
+
168
+ Il file `config.yaml` ha cinque sezioni di primo livello: `app`, `broker`, `topics`, `auth-providers`, `gateway`.
55
169
 
170
+ ### `app`
171
+
172
+ ```yaml
56
173
  app:
57
- environment: development
174
+ port: 3000
175
+ host: 0.0.0.0
176
+ environment: development # 'development' | 'production' (controlla il dettaglio degli errori esposti)
177
+ ```
178
+
179
+ > In `production` gli errori restituiti dal gateway sono ridotti a `{ message, name }`; in `development` viene incluso lo stack/dettaglio. Vedi `UtilsService.error2Object`.
58
180
 
181
+ ### `broker`
182
+
183
+ ```yaml
59
184
  broker:
60
- uri: "amqp://localhost/<vhost>"
61
- defaultSubscribeErrorBehavior: "ack"
62
- defaultPublishErrorBehavior: "reject"
63
- connectionManagerOptions:
185
+ name: rabbitmq
186
+ uri: "amqp://user:pass@localhost:5672/vhost" # stringa o array di URI (failover)
187
+ prefetchCount: 10
188
+ defaultRpcTimeout: 10000 # ms, default per requestData
189
+ defaultSubscribeErrorBehavior: ack # ack | reject | requeue (comportamento di default sugli errori consumer)
190
+
191
+ connectionManagerOptions: # opzioni amqp-connection-manager
64
192
  heartbeatIntervalInSeconds: 60
65
193
  reconnectTimeInSeconds: 60
66
194
  connectionOptions:
67
195
  clientProperties:
68
- connection_name: "connection-name"
196
+ connection_name: my-service # OBBLIGATORIO per broadcast e per il gateway WebSocket
69
197
  credentials:
70
- mechanism: PLAIN
198
+ mechanism: PLAIN # PLAIN | EXTERNAL | AMQPLAIN
71
199
  username: guest
72
200
  password: guest
73
201
 
74
202
  exchanges:
75
203
  - name: users-ex
76
- type: direct
77
- createExchangeIfNotExists: true
78
- options:
79
- durable: true
204
+ type: direct # direct | topic | fanout | headers
205
+ createExchangeIfNotExists: true # false → checkExchange (deve già esistere)
206
+ options: { durable: true }
80
207
 
81
208
  queues:
82
209
  - name: users-rpc-q
83
210
  exchange: users-ex
84
- routingKey: users.rpc
211
+ routingKey: users.rpc # string | string[]; OBBLIGATORIO se exchange è di tipo `topic`
85
212
  createQueueIfNotExists: true
86
- options:
87
- durable: true
213
+ options: { durable: true }
214
+
215
+ replyQueues: # mappa exchange → reply queue per le risposte RPC
216
+ users-ex: users-reply-q # se omesso si usa la direct-reply-to di RabbitMQ
217
+ ```
218
+
219
+ ### `topics`
88
220
 
89
- replyQueues:
90
- users-ex: users-reply-q
221
+ Un topic mappa un nome logico (azione/microservizio) su un percorso AMQP. Il `mode` decide la semantica.
222
+
223
+ | `mode` | Quando usarlo | Campi richiesti | Semantica |
224
+ | ----------- | -------------------------------------- | ------------------------------------------------------- | ------------------------------------ |
225
+ | `rpc` | request/response | `name`, `queue` (o `exchange`+`routingKey`) | risposta immediata + timeout |
226
+ | `handle` | worker su una coda | `name`, `queue` | consumer di coda semplice |
227
+ | `broadcast` | un messaggio a molti consumer | `name`, `exchange`, `routingKey` | fanout/topic; richiede `connection_name` |
228
+ | `event` | publish senza risposta | `name`, `queue` **oppure** `exchange`+`routingKey` | fire-and-forget |
91
229
 
230
+ ```yaml
92
231
  topics:
93
232
  - name: users-rpc
94
233
  mode: rpc
95
- queue: users-rpc-q
234
+ queue: users-rpc-q # deve esistere in broker.queues[]
235
+
236
+ - name: invoice-handle
237
+ mode: handle
238
+ queue: invoice-handle-q
239
+
240
+ - name: notify-broadcast
241
+ mode: broadcast
242
+ exchange: notify-ex
243
+ routingKey: notify.#
244
+
245
+ - name: audit-event
246
+ mode: event
247
+ exchange: audit-ex
248
+ routingKey: audit.created
249
+ ```
250
+
251
+ > `toObservable: true` su un topic `handle` instrada i messaggi su `BrokerService.events$` (Observable RxJS) invece che a un handler registrato.
252
+
253
+ ### `auth-providers`
254
+
255
+ Provider di autenticazione usati dalle route del gateway (`gateway.paths[].auth`) e dagli eventi WebSocket (`gateway.events[].auth`).
256
+
257
+ ```yaml
258
+ auth-providers:
259
+ - name: gateway-jwks
260
+ type: jwks # jwt | jwks | basic | str-compare | none
261
+ issuer: https://issuer.example.com/realms/main
262
+ jwksUri: https://issuer.example.com/certs
263
+ algorithms: [RS256]
264
+ httpsAllowUnauthorized: false # true SOLO per issuer self-signed in dev
265
+ jwtMap: # claim del token → claim mappato (header-prefixed)
266
+ - sub:userId
267
+ - roles:roles
268
+ headerPrefix: X-GTW-AUTH- # prefisso degli header propagati ai microservizi
269
+ uidClaim: USERID # dest (uppercase) usato come user id per l'ACL
270
+ usernameClaim: USERNAME
271
+ aclTopic: acl # topic RPC interrogato per i ruoli
272
+ aclAction: can-user-do
273
+
274
+ - name: gateway-jwt
275
+ type: jwt
276
+ secret: your-jwt-secret
277
+ issuer: https://issuer.example.com/realms/main
278
+ audience: your-audience
279
+ algorithms: [HS256]
280
+ jwtMap: [sub:userId, roles:roles]
281
+ headerPrefix: X-GTW-AUTH-
282
+ uidClaim: USERID
283
+ usernameClaim: USERNAME
284
+ aclTopic: acl
285
+ aclAction: can-user-do
286
+
287
+ - name: gateway-basic
288
+ type: basic
289
+ clientId: my-user
290
+ clientSecret: my-pass
291
+ headerPrefix: X-GTW-AUTH-
292
+
293
+ - name: gateway-str
294
+ type: str-compare
295
+ secret: your-static-token
296
+ headerPrefix: Bearer # prefisso atteso nell'header Authorization
297
+ ```
298
+
299
+ Mapping dei claim: un token con `{ sub: "u_1", roles: [...] }` e `jwtMap: [sub:userId]`, `headerPrefix: X-GTW-AUTH-` produce l'header `X-GTW-AUTH-USERID = u_1` propagato al microservizio. Leggilo con `@BrokerParam('header', 'X-GTW-AUTH-USERID')`.
300
+
301
+ > **Sicurezza dei provider**: `algorithms` è **obbligatorio** per `jwt`/`jwks` (se omesso la verifica è negata → previene l'algorithm-confusion); per `jwks` solo algoritmi asimmetrici (RS\*/ES\*/PS\*), `HS*`/`none` rifiutati. `str-compare` senza `secret` e `basic` senza `clientSecret` fanno **pass-through** (richiesta considerata autenticata — provider di fatto aperto/disabilitato; usalo consapevolmente). Senza `jwtMap` i claim vengono propagati non mappati: definiscilo sempre.
302
+
303
+ ### `gateway`
96
304
 
305
+ ```yaml
97
306
  gateway:
98
307
  mode: gateway
99
- paths:
100
- - name: users-create
101
- method: POST
102
- path: /users
103
- dataSource: body
104
- topic: users-rpc
105
- action: user.create
106
- mode: rpc
107
- events: []
308
+ headerPrefix: X-GTW- # prefisso per gli header inoltrati (forwardHeaders)
309
+
310
+ ws: # opzioni WebSocket — solo livello connessione
311
+ maxConnections: 5000
312
+ maxSubscriptionsPerClient: 50
313
+ heartbeatIntervalMs: 30000
314
+ # auth/roles/scope sono dichiarati PER-EVENTO (events[].auth/requireAuth/roles/...)
315
+
316
+ loadConfig: # caricamento remoto di paths/events via RPC (opzionale)
317
+ paths: { topic: gtw.config, action: get-paths }
318
+ events: { topic: gtw.config, action: get-events }
319
+
320
+ paths: [ ... ] # vedi "Gateway HTTP"
321
+ events: [ ... ] # vedi "Gateway WebSocket"
108
322
  ```
109
323
 
110
- ## Quick usage
324
+ ---
325
+
326
+ ## Scrivere un microservizio (AMQP)
111
327
 
112
- ### RPC handler with decorators
328
+ ### Handler con i decoratori
113
329
 
114
330
  ```ts
115
331
  import { Injectable } from '@nestjs/common';
@@ -117,6 +333,8 @@ import { BrokerAction, BrokerParam } from '@open-rlb/nestjs-amqp';
117
333
 
118
334
  @Injectable()
119
335
  export class UsersActionService {
336
+ // @BrokerAction(topic, action, type?) — il `type` è documentativo: l'handler è
337
+ // SEMPRE raggiungibile sia in rpc sia in event (vedi "Doppio comportamento").
120
338
  @BrokerAction('users-rpc', 'user.create', 'rpc')
121
339
  async createUser(
122
340
  @BrokerParam('body', 'email') email: string,
@@ -128,100 +346,105 @@ export class UsersActionService {
128
346
  }
129
347
  ```
130
348
 
131
- ### RPC call from code
349
+ Registra il servizio come provider in un modulo NestJS qualunque: il `MetadataScannerService` lo scopre all'avvio e registra automaticamente il consumer per il topic.
132
350
 
133
- ```ts
134
- import { Injectable } from '@nestjs/common';
135
- import { BrokerService } from '@open-rlb/nestjs-amqp';
351
+ #### Sorgenti `@BrokerParam(source, name?)`
136
352
 
137
- @Injectable()
138
- export class UsersClientService {
139
- constructor(private readonly broker: BrokerService) {}
353
+ | Source | Valore iniettato |
354
+ | ----------- | ----------------------------------------------------- |
355
+ | `body` | `payload[name ?? nomeParametro]` |
356
+ | `body-full` | payload completo |
357
+ | `header` | `headers[name ?? nomeParametro]` |
358
+ | `tag` | consumer tag AMQP |
359
+ | `action` | action del messaggio |
360
+ | `topic` | topic corrente |
140
361
 
141
- async createUser() {
142
- return this.broker.requestData(
143
- 'users-rpc',
144
- 'user.create',
145
- { email: 'john@example.com', role: 'admin' },
146
- { 'X-Tenant': 'acme' },
147
- 5000,
148
- );
149
- }
150
- }
362
+ > Se ometti `@BrokerParam` su un parametro, il default è `{ source: 'body' }` con chiave = nome del parametro.
363
+
364
+ ### Doppio comportamento RPC / event
365
+
366
+ Ogni `@BrokerAction` è eseguibile **sia in RPC sia in event**, senza modifiche al servizio. Cambia solo cosa attende il chiamante.
367
+
368
+ | Modalità | Come si invoca | Cosa si attende |
369
+ | -------- | ------------------------------------------------- | ------------------------------------------------------- |
370
+ | `rpc` | `broker.requestData(...)` / path `mode: rpc` | la **risposta** del metodo (request/response) |
371
+ | `event` | `broker.publishMessage(...)` / path `mode: event` | solo che il **broker prenda in carico** (publisher confirm) |
372
+
373
+ `publishMessage` è `async` e si risolve solo al publisher confirm (rigetta su nack/errore). Sul gateway, una path `mode: event` risponde `202` **dopo** il confirm e `503` se il broker non accetta.
374
+
375
+ ```yaml
376
+ # Lo stesso topic/action esposto nei due modi
377
+ gateway:
378
+ paths:
379
+ - { name: users-create-sync, method: POST, path: /users, topic: users-rpc, action: user.create, mode: rpc }
380
+ - { name: users-create-async, method: POST, path: /users/async, topic: users-rpc, action: user.create, mode: event }
151
381
  ```
152
382
 
153
- ### Manual consumer (without decorators)
383
+ ### Consumer manuali (senza decoratori)
154
384
 
155
385
  ```ts
386
+ // RPC
156
387
  await broker.registerRpc<{ id: string }, { ok: boolean }>('health-rpc', async (event) => {
157
388
  return { ok: !!event.payload?.id };
158
389
  });
159
390
 
391
+ // handle / broadcast (gli handler devono restituire void)
160
392
  await broker.registerHandler<{ invoiceId: string }>('invoice-handle', async (event) => {
161
393
  console.log(event.payload.invoiceId);
162
394
  });
163
395
  ```
164
396
 
165
- ## Quick reference
397
+ ### Pubblicare / chiamare da codice
166
398
 
167
- ### `BrokerService`
168
-
169
- | Method | Use |
170
- | ---------------------------------------------------------- | ------------------------------- |
171
- | `requestData(topic, action, payload?, headers?, timeout?)` | RPC request/response |
172
- | `registerRpc(topic, handler)` | manual RPC consumer |
173
- | `registerHandler(topic, handler)` | `handle` / `broadcast` consumer |
174
-
175
- ### Topic types (`topics[].mode`)
176
-
177
- | Type | Use it when | Minimal config | Why |
178
- | ----------- | -------------------------------------- | ------------------------------------------------------- | ------------------------------------ |
179
- | `rpc` | you need request/response | `name`, `mode: rpc`, `queue` (or `exchange+routingKey`) | immediate response + timeout control |
180
- | `handle` | you need a worker on one queue | `name`, `mode: handle`, `queue` | simple queue consumer |
181
- | `broadcast` | you need one message to many consumers | `name`, `mode: broadcast`, `exchange`, `routingKey` | fanout/topic pattern |
182
- | `event` | you need publish without response | `name`, `mode: event`, `queue` or `exchange+routingKey` | fire-and-forget (not covered here) |
183
-
184
- Quick snippet:
185
-
186
- ```yaml
187
- topics:
188
- - name: users-rpc
189
- mode: rpc
190
- queue: users-rpc-q
399
+ ```ts
400
+ @Injectable()
401
+ export class UsersClient {
402
+ constructor(private readonly broker: BrokerService) {}
191
403
 
192
- - name: invoice-handle
193
- mode: handle
194
- queue: invoice-handle-q
404
+ // RPC: attende la risposta
405
+ createUserRpc() {
406
+ return this.broker.requestData('users-rpc', 'user.create',
407
+ { email: 'a@b.c', role: 'admin' }, { 'X-Tenant': 'acme' }, 5000);
408
+ }
195
409
 
196
- - name: notify-broadcast
197
- mode: broadcast
198
- exchange: notify-ex
199
- routingKey: notify.#
410
+ // Event: attende solo che il broker prenda in carico
411
+ async emitAudit() {
412
+ await this.broker.publishMessage('audit-event', 'audit.created', { entity: 'user', id: 'u_1' });
413
+ }
414
+ }
200
415
  ```
201
416
 
202
- ### Decorators
417
+ ---
203
418
 
204
- | Decorator | Use |
205
- | ------------------------------------------------------------- | ------------------------------------ |
206
- | `@BrokerAction(topic, action, type?)` | binds method to topic/action |
207
- | `@BrokerParam(source, name?)` | maps method params from message data |
208
- | `@BrokerAuth(authName, allowAnonymous?, roles?)` | auth metadata |
209
- | `@BrokerHTTP(method, path, dataSource?, timeout?, parseRaw?)` | HTTP metadata |
419
+ ## Gateway HTTP
210
420
 
211
- ### `@BrokerParam` sources
421
+ Le route sono dichiarate in `gateway.paths[]` e registrate dinamicamente su Express al boot.
212
422
 
213
- | Source | Injected value |
214
- | ----------- | --------------------------------- |
215
- | `body` | `payload[name or parameter name]` |
216
- | `body-full` | full payload |
217
- | `header` | `headers[name or parameter name]` |
218
- | `tag` | AMQP consumer tag |
219
- | `action` | message action |
220
- | `topic` | current topic |
423
+ ```yaml
424
+ gateway:
425
+ paths:
426
+ - name: users-create
427
+ method: POST # GET | POST | PUT | DELETE | PATCH
428
+ path: /users/:tenant? # supporta route param Express
429
+ dataSource: body # body | query | params | body-query | query-body
430
+ topic: users-rpc
431
+ action: user.create
432
+ mode: rpc # rpc | event
433
+ timeout: 7000 # solo rpc
434
+ auth: gateway-jwks # nome di un auth-provider
435
+ allowAnonymous: false # true → consente l'accesso anche senza auth valida
436
+ roles: [users.create] # richiede un IAclRoleService registrato
437
+ successStatusCode: 201
438
+ binary: false # true → risposta come Buffer base64-decoded
439
+ redirect: 302 # se valorizzato, redirect alla URL contenuta nella risposta
440
+ headers: { Cache-Control: no-store } # header statici sulla risposta
441
+ forwardHeaders: { Tenant: x-tenant } # header della richiesta da inoltrare al microservizio
442
+ parseRaw: false # true → inoltra il body raw come $raw (richiede rawBody:true nel bootstrap)
443
+ ```
221
444
 
222
- ### `gateway.paths[].dataSource`
445
+ #### Composizione del payload (`dataSource`)
223
446
 
224
- | Value | Payload |
447
+ | Valore | Payload inviato al broker |
225
448
  | ------------ | -------------------------------- |
226
449
  | `body` | `{...params, ...body}` |
227
450
  | `query` | `{...params, ...query}` |
@@ -229,72 +452,252 @@ topics:
229
452
  | `body-query` | `{...params, ...query, ...body}` |
230
453
  | `query-body` | `{...params, ...body, ...query}` |
231
454
 
232
- ### Auth providers
455
+ > I route param (`req.params`) vengono **ri-applicati per ultimi** su `data`: a parità di chiave vincono sempre sul body/query. Gli upload multipart finiscono in `$files`; il body raw (se `parseRaw`) in `$raw`.
456
+
457
+ #### Mappatura errori → status HTTP
233
458
 
234
- #### Type: `jwks`
459
+ Il `name` dell'errore lanciato dal microservizio determina lo status: `BadRequestError`/`InvalidParamsErrror` → 400, `UnauthorizedError` → 401, `ForbiddenError` → 403, `NotFoundError` → 404, altrimenti → 500. In `mode: event` un confirm fallito → 503.
460
+
461
+ ---
462
+
463
+ ## Gateway WebSocket
464
+
465
+ Il gateway WebSocket inoltra eventi del broker ai client connessi (o a webhook HTTP), con autenticazione, autorizzazione per evento e funzionamento corretto in **multi-istanza** (fan-out).
466
+
467
+ ### Configurazione
235
468
 
236
469
  ```yaml
237
- auth-providers:
238
- - name: gateway-jwks
239
- type: jwks
240
- issuer: https://issuer.example.com/realms/main
241
- jwksUri: https://issuer.example.com/certs
242
- algorithms: [RS256]
243
- jwtMap:
244
- - sub:userId
245
- - roles:roles
246
- headerPrefix: X-GTW-AUTH-
247
- uidClaim: USERID
248
- usernameClaim: USERNAME
249
- aclTopic: acl
250
- aclAction: can-user-do
470
+ gateway:
471
+ ws: # solo livello connessione
472
+ maxConnections: 5000 # limite connessioni per istanza
473
+ maxSubscriptionsPerClient: 50 # limite sottoscrizioni per client
474
+ heartbeatIntervalMs: 30000 # ping/pong per chiudere le connessioni morte
475
+ allowedOrigins: # allowlist Origin dell'handshake (omessa → tutte)
476
+ - https://app.example.com
477
+ maxMessageBytes: 16384 # scarta i frame client più grandi (default 16KB)
478
+
479
+ events:
480
+ - name: orders
481
+ type: ws # ws | http (webhook)
482
+ exchange: orders-ex
483
+ routingKey: orders.#
484
+ auth: gateway-jwks # provider che verifica il token e mappa i claim PER QUESTO evento
485
+ requireAuth: true # default true quando `auth` è impostato; false → auth opzionale
486
+ roles: [orders.read] # verifica ACL via IAclRoleService
487
+ scopeClaim: X-GTW-AUTH-USERID # inoltra solo i messaggi dell'utente...
488
+ payloadKey: userId # ...dove payload.userId === claim dell'utente
489
+
490
+ - name: invoices # forwarding webhook
491
+ type: http
492
+ exchange: inv-ex
493
+ routingKey: inv.#
494
+ url: https://hooks.example.com/invoices
495
+ method: POST
496
+ timeout: 8000
251
497
  ```
252
498
 
253
- #### Type: `jwt`
499
+ ### Autenticazione (token nel subprotocol)
254
500
 
255
- ```yaml
256
- auth-providers:
257
- - name: gateway-jwt
258
- type: jwt
259
- secret: your-jwt-secret
260
- issuer: https://issuer.example.com/realms/main
261
- audience: your-audience
262
- algorithms: [HS256]
263
- jwtMap:
264
- - sub:userId
265
- - roles:roles
266
- headerPrefix: X-GTW-AUTH-
267
- uidClaim: USERID
268
- usernameClaim: USERNAME
269
- aclTopic: acl
270
- aclAction: can-user-do
501
+ I browser non possono impostare header custom sull'handshake, quindi il token JWT viaggia nel **subprotocol** (`Sec-WebSocket-Protocol`):
502
+
503
+ ```js
504
+ const ws = new WebSocket('ws://localhost:3000', [token]); // oppure ['bearer', token]
271
505
  ```
272
506
 
273
- #### Type: `str-compare`
507
+ Il token viene conservato sulla connessione e **verificato al momento del `subscribe` con il provider dichiarato dall'evento** (`events[].auth`), che ne mappa anche i claim. La verifica è memoizzata per provider: lo stesso token è verificato al più una volta per provider. Eventi diversi possono usare provider diversi.
274
508
 
275
- ```yaml
276
- auth-providers:
277
- - name: gateway-str
278
- type: str-compare
279
- secret: your-static-token
280
- headerPrefix: Bearer
509
+ ### Protocollo client
510
+
511
+ ```js
512
+ ws.send(JSON.stringify({ action: 'subscribe', topic: 'orders', select: { status: 'open' } }));
513
+ ws.send(JSON.stringify({ action: 'unsubscribe', topic: 'orders' }));
514
+ // messaggi in arrivo: { topic: 'onOrders', data: <payload> }
515
+ // errori: { topic: 'onError', data: { event, error } }
281
516
  ```
282
517
 
283
- ### Gateway path (RPC)
518
+ ### Sicurezza e scalabilità
284
519
 
285
- ```yaml
286
- - name: users-create
287
- method: POST
288
- path: /users
289
- dataSource: body
290
- topic: users-rpc
291
- action: user.create
292
- mode: rpc
293
- timeout: 7000
520
+ - **Auth per evento**: `events[].auth` indica il provider che verifica il token e mappa i claim per quell'evento; `requireAuth: false` rende l'auth opzionale (anonimi ammessi, claim mappati se il token c'è). Subscribe negato (`onError: unauthorized`) se l'auth è richiesta e il token non è valido.
521
+ - **Authz per evento**: `roles` (ACL via `IAclRoleService`) sull'identità ricavata da `auth`.
522
+ - **Scoping per-utente**: `scopeClaim` + `payloadKey` impediscono a un client di ricevere dati altrui tramite un `select` arbitrario (il filtro server-side è intersecato con quello del client, mai allargato). Se `scopeClaim` è impostato senza `payloadKey`, **nega tutto** (safe default).
523
+ - **Sessione limitata dalla scadenza del token**: l'`exp` del JWT viene catturato alla prima verifica e la connessione viene chiusa (`1008 token expired`) appena scade — niente consegne dopo la scadenza.
524
+ - **Origin allowlist**: `gateway.ws.allowedOrigins` rifiuta gli handshake cross-site (se omessa, tutte le origin sono accettate e lo si segnala a boot).
525
+ - **Multi-istanza**: ogni istanza crea una coda AMQP **effimera ed esclusiva** (nome unico per processo) → tutte le repliche ricevono ogni evento e lo inoltrano ai rispettivi client.
526
+ - **Hardening**: heartbeat ping/pong, limiti connessioni/sottoscrizioni, limite dimensione frame (`maxMessageBytes`), cleanup robusto su `close`/`error`.
527
+
528
+ ---
529
+
530
+ ## Remote config
531
+
532
+ `RemoteConfigService` permette ai microservizi di **registrare le proprie route nel gateway a runtime**, pubblicando le loro `PathDefinition` su un exchange fanout `config.ms`. Il gateway le riceve e chiama `HttpHandlerService.registerPath()` dinamicamente. In alternativa, `gateway.loadConfig` carica paths/events tramite una singola chiamata RPC all'avvio.
533
+
534
+ ---
535
+
536
+ ## Moduli opzionali `AclModule` e `GatewayAdminModule` (persistenza fornita dal consumer)
537
+
538
+ Due moduli **opzionali** per gestire ACL e configurazione gateway a database. **La lib non dipende da Mongo/Redis**: definisce i servizi/cache + i **contratti repository (classi astratte)** e l'interfaccia `AclCacheStore`; **il consumer fornisce le implementazioni** (es. Mongo + Redis). Esempio completo e funzionante: **[`apps/gateway-2`](apps/gateway-2)** — per restare autonomo usa **repository in-RAM** (`InMemory*Repository`) e una **cache L2 in-RAM** (`InMemoryAclStore`), così gira solo con RabbitMQ; in produzione si rimpiazzano con implementazioni Mongo/Redis senza toccare la lib.
539
+
540
+ ### `AclModule` — ACL DB-backed con cache 2-livelli
541
+
542
+ ACL (azioni → ruoli → grant per-utente) con `canUserDo` corretto e **cache RAM + L2 pluggable** (TTL diversi) e invalidazione che forza il DB.
543
+
544
+ ```ts
545
+ import { AclModule, AclService, AclActionRepository, AclRoleRepository, AclGrantRepository,
546
+ RLB_ACL_CACHE_STORE, RLB_GTW_ACL_ROLE_SERVICE } from '@open-rlb/nestjs-amqp';
547
+
548
+ @Module({
549
+ imports: [
550
+ BrokerModule.forRootAsync({ /* ... */ }),
551
+ // ProxyModule riceve auth/gateway config e usa AclService come IAclRoleService (AclModule è @Global):
552
+ ProxyModule.forRootAsync({
553
+ imports: [ConfigModule],
554
+ inject: [ConfigService],
555
+ useFactory: (config: ConfigService) => ({
556
+ authOptions: config.get<HandlerAuthConfig[]>('auth-providers'),
557
+ gatewayOptions: config.get<GatewayConfig>('gateway'),
558
+ }),
559
+ providers: [{ provide: RLB_GTW_ACL_ROLE_SERVICE, useExisting: AclService }],
560
+ }),
561
+ AclModule.forRoot(
562
+ [
563
+ ...aclMongoModelProviders, // provider dei model Mongoose
564
+ { provide: AclActionRepository, useClass: MongoAclActionRepository },
565
+ { provide: AclRoleRepository, useClass: MongoAclRoleRepository },
566
+ { provide: AclGrantRepository, useClass: MongoAclGrantRepository },
567
+ InMemoryAclStore, // implementa AclCacheStore
568
+ { provide: RLB_ACL_CACHE_STORE, useExisting: InMemoryAclStore },// L2 opzionale (omesso → solo RAM)
569
+ ],
570
+ { cache: { ramTtlMs: 30000, l2TtlSec: 600 } },
571
+ ),
572
+ ],
573
+ })
574
+ export class AppModule {}
575
+ ```
576
+
577
+ - I handler sono esposti su `BrokerService` con topic **`rlb-acl`** (costante `ACL_TOPIC`): `acl-can-user-do` (rpc), `acl-grant`/`acl-revoke`, `acl-action-*`, `acl-role-*`. Definisci nel tuo `broker.topics` un topic `rlb-acl` e imposta negli auth-provider `aclTopic: rlb-acl`, `aclAction: acl-can-user-do`.
578
+ - `AclService.canUserDo(topic, action, userId)` serve dalla cache; sul miss interroga il DB (`checkActions`: i ruoli del grant devono coprire l'azione) e ripopola RAM+L2.
579
+ - **Invalidazione**: ogni mutazione (grant/role/action) svuota L1 e L2 → la prossima verifica pesca dal DB. Senza L2, la coerenza multi-istanza è limitata dal `ramTtlMs`.
580
+ - **Cache L2 pluggable**: il consumer fornisce `{ provide: RLB_ACL_CACHE_STORE, useClass/useExisting }` che implementa `AclCacheStore` (`get/set/del/keys`). In `gateway-2` è `InMemoryAclStore` (mock in RAM, nessuna dipendenza esterna); in produzione plugga uno store condiviso (es. Redis).
581
+
582
+ ### `GatewayAdminModule` — CRUD rotte/auth + liste + metriche
583
+
584
+ CRUD di rotte HTTP e auth-providers (repo forniti dal consumer), con **liste esportabili** per il gateway (in aggiunta allo YAML), **metriche a contatori** e **ordinamento path static-before-param**.
585
+
586
+ ```ts
587
+ import { GatewayAdminModule, HttpPathRepository, AuthProviderRepository, HttpMetricRepository } from '@open-rlb/nestjs-amqp';
588
+
589
+ GatewayAdminModule.forRoot([
590
+ ...gatewayAdminMongoModelProviders,
591
+ { provide: HttpPathRepository, useClass: MongoHttpPathRepository },
592
+ { provide: AuthProviderRepository, useClass: MongoAuthProviderRepository },
593
+ { provide: HttpMetricRepository, useClass: MongoHttpMetricRepository },
594
+ ]);
294
595
  ```
295
596
 
296
- ## Common errors
597
+ Handler su topic **`rlb-gateway-admin`** (`GATEWAY_ADMIN_TOPIC`):
598
+ - CRUD rotte: `gw-path-create/update/delete/get/list`; **`gw-path-export` (rpc)** → tutte le rotte abilitate come `PathDefinition[]` **ordinate** (statiche prima delle parametriche). Punta `gateway.loadConfig.paths` a `{ topic: rlb-gateway-admin, action: gw-path-export }`.
599
+ - CRUD auth: `gw-auth-create/.../list`; **`gw-auth-export` (rpc)** → `HandlerAuthConfig[]` abilitati (per frontend / merge lato gateway).
600
+ - Metriche: **`gw-metrics-track` (event)** incrementa i contatori per `(method, route)`; **`gw-metrics-get` (rpc)** restituisce count/errori/durata media per il frontend.
601
+
602
+ > **Ordinamento path**: `gw-path-export` usa `orderPaths()` così `resources/path` precede `resources/:varName` — necessario perché Express, registrando prima la rotta parametrica, intercetterebbe il segmento statico.
603
+
604
+ ---
605
+
606
+ ## API `BrokerService`
607
+
608
+ | Metodo | Uso |
609
+ | ------------------------------------------------------------ | ------------------------------------------------ |
610
+ | `requestData(topic, action, payload?, headers?, timeout?)` | RPC request/response (attende la risposta) |
611
+ | `publishMessage(topic, action, payload, headers?)` → `Promise<boolean>` | event fire-and-forget con publisher confirm |
612
+ | `registerRpc(topic, handler)` | consumer RPC manuale |
613
+ | `registerHandler(topic, handler)` | consumer `handle` / `broadcast` (ritorna void) |
614
+ | `getRpc(topic)` / `getHandler(topic)` | recupera l'handler registrato |
615
+ | `events$` / `getEvents$<T>()` | Observable degli eventi dei topic `toObservable` |
616
+
617
+ ### Decoratori
618
+
619
+ | Decoratore | Uso |
620
+ | ------------------------------------------------------------- | ------------------------------------ |
621
+ | `@BrokerAction(topic, action, type?)` | lega un metodo a topic/action |
622
+ | `@BrokerParam(source, name?)` | mappa i parametri dai dati messaggio |
623
+ | `@BrokerAuth(authName, allowAnonymous?, roles?)` | metadati di auth (usati dallo scanner) |
624
+ | `@BrokerHTTP(method, path, dataSource?, timeout?, parseRaw?)` | metadati HTTP (usati dallo scanner) |
625
+
626
+ ### Pipe utility
627
+
628
+ `BooleanPipe` e `NumberPipe` convertono valori stringa/numerici (es. da query string). Esportate da `@open-rlb/nestjs-amqp`.
629
+
630
+ ---
631
+
632
+ ## ⚠️ Gotcha e casi a rischio bug
633
+
634
+ Questi sono i punti che causano più frequentemente bug silenziosi. **Leggili prima di estendere la lib.**
635
+
636
+ ### Decoratori e handler
637
+
638
+ 1. **Niente destructuring nei parametri dell'handler.** `@BrokerParam` associa i parametri leggendo il *source* della funzione con una regex (`getParamNames`). Una firma come `fn({ a, b })` rompe l'allineamento degli indici. Usa parametri semplici.
639
+ 2. **Evita i valori di default nei parametri.** C'è uno strip basilare (`removeDefaultsFromParams`), ma default complessi (oggetti, chiamate) disallineano la mappatura. Passa sempre un `name` esplicito a `@BrokerParam`.
640
+ 3. **`(topic, action)` deve essere unico.** Tutti gli `@BrokerAction` dello stesso topic condividono **una sola coda/consumer** e vengono smistati per `action`. Due metodi con lo stesso `(topic, action)` → il secondo sovrascrive il primo in silenzio.
641
+
642
+ ### Wiring topic ↔ queue ↔ exchange
643
+
644
+ 4. **Il `name` del topic deve coincidere ovunque**: `@BrokerAction(topic)`, `topics[].name`, `requestData/publishMessage(topic)`, `gateway.paths[].topic`/`events[]`. Un typo → `Topic X not found in configuration`.
645
+ 5. **`mode: rpc`/`handle` richiedono che `topics[].queue` esista in `broker.queues[]`**, e che il `queue.exchange` esista in `broker.exchanges[]`. In `handle` un queue mancante causa un NPE all'avvio (`queue.exchange`).
646
+ 6. **Exchange `type: topic` → il queue DEVE avere `routingKey`**, altrimenti l'avvio lancia `Queue ... has no routing key`.
647
+ 7. **`mode: broadcast` e gateway WebSocket richiedono `connection_name`** (`clientProperties.connection_name`), altrimenti throw.
648
+
649
+ ### RPC / timeout / errori
650
+
651
+ 8. **Reply RPC**: `requestData` risolve `replyTo` da `broker.replyQueues[exchange]`; se assente usa la direct-reply-to di RabbitMQ. Un `replyQueues` con la chiave exchange sbagliata → nessuna risposta → timeout.
652
+ 9. **Le eccezioni dell'handler RPC NON propagano come throw lato consumer**: vengono restituite come `{ success: false, error }` e `requestData` rilancia l'errore al chiamante. Sul gateway lo status dipende dal `error.name` (vedi tabella). Dai agli errori un `name` coerente.
653
+ 10. **Timeout di default 10s** (o `broker.defaultRpcTimeout`). Per RPC lente imposta `timeout` sulla path o sull'argomento di `requestData`.
654
+
655
+ ### Gateway HTTP
656
+
657
+ 11. **`parseRaw: true` richiede `NestFactory.create(AppModule, { rawBody: true })`**, altrimenti `$raw` è `undefined`.
658
+ 12. **I route param vincono sul body/query** (ri-applicati per ultimi). Attento alle collisioni di chiave (`:id` vs `body.id`).
659
+ 13. **Gli upload sono in `$files`** (multer `.any()`); i buffer vengono convertiti in stringa binaria — rigestiscili con cura lato consumer.
660
+
661
+ ### Auth / ACL
662
+
663
+ 14. **`roles` su una path o evento richiede un `IAclRoleService`** registrato via `RLB_GTW_ACL_ROLE_SERVICE` in `ProxyModule.forRootAsync({ providers: [...] })`. L'auth-provider deve definire `aclTopic`, `aclAction`, `uidClaim`, `usernameClaim`, e `uidClaim` deve corrispondere a un `dest` del `jwtMap`. Mancante → throw. Nota: `authOptions`/`gatewayOptions` si passano a `ProxyModule`, non a `BrokerModule`.
664
+ 15. **Gli header propagati sono uppercase e prefissati** (`${headerPrefix}${DEST}`): leggi `X-GTW-AUTH-USERID`, non `userId`.
665
+
666
+ ### WebSocket
667
+
668
+ 16. **`scopeClaim` referenzia il claim MAPPATO** (con `headerPrefix`, es. `X-GTW-AUTH-USERID`), non il claim grezzo del token. `payloadKey` è la chiave nel payload dell'evento. Senza `payloadKey`, lo scope nega tutto.
669
+ 17. **Non usare code durevoli condivise per gli eventi WS**: la lib crea una coda esclusiva per istanza apposta per il fan-out. Una coda fissa farebbe competere le istanze (i client di un'istanza perderebbero messaggi).
670
+
671
+ ### Publish / event
672
+
673
+ 18. **`publishMessage` è `async`: devi fare `await`** per ottenere la garanzia di publisher confirm e per intercettare i fallimenti. Senza `await` è fire-and-forget senza garanzia.
674
+ 19. **Gli handler `handle`/`broadcast` devono restituire `void`**: un valore di ritorno genera un warning (`Subscribe handlers should only return void`).
675
+
676
+ ### TLS / credenziali
677
+
678
+ 20. **JWKS verifica il TLS di default.** Usa `httpsAllowUnauthorized: true` su un provider solo per issuer self-signed in sviluppo.
679
+ 21. **`mechanism` credenziali**: `PLAIN` | `EXTERNAL` | `AMQPLAIN` (case-insensitive). Un valore sconosciuto non imposta la `response` → autenticazione fallita.
680
+
681
+ ---
682
+
683
+ ## Errori comuni
684
+
685
+ - `Topic <name> not found in configuration`: controlla `topics[].name`, `@BrokerAction`, `requestData`/`publishMessage`, `gateway.paths[].topic`.
686
+ - `Queue <name> not found in configuration`: verifica che `topics[].queue` esista in `broker.queues[]`.
687
+ - `Queue <name> has no routing key`: l'exchange è di tipo `topic` ma il queue non ha `routingKey`.
688
+ - `Client name is required ...`: manca `connection_name` (richiesto da broadcast e WebSocket).
689
+ - `ACL Role Service not found`: stai usando `roles` senza aver registrato `RLB_GTW_ACL_ROLE_SERVICE`.
690
+ - `401/403` dal gateway: controlla `auth`, `auth-providers[]`, e l'ACL service quando usi `roles`.
691
+ - Timeout RPC: `replyQueues` errato, `action` non gestita da alcun servizio, o handler troppo lento (`timeout`).
692
+
693
+ ---
694
+
695
+ ## Sviluppo
696
+
697
+ ```bash
698
+ npm run build # compila (tsc)
699
+ npm test # jest
700
+ npm run start:dev # nest start --watch (app gateway di esempio)
701
+ ```
297
702
 
298
- - `Topic <name> not found in configuration`: check `topics[].name`, `@BrokerAction`, `requestData`, `gateway.paths[].topic`.
299
- - `Queue <name> not found in configuration`: check that `topics[].queue` exists in `broker.queues[]`.
300
- - `401/403` from gateway: check `gateway.paths[].auth`, `auth-providers[]`, ACL service when using `roles`.
703
+ Licenza: MIT.