@open-rlb/nestjs-amqp 2.0.4 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +4 -2
  2. package/amqp-lib/config/rabbitmq.config.d.ts +6 -0
  3. package/modules/acl/const.d.ts +0 -1
  4. package/modules/acl/const.js +0 -1
  5. package/modules/acl/const.js.map +1 -1
  6. package/modules/acl/models.d.ts +5 -7
  7. package/modules/acl/repository/acl-action.repository.d.ts +1 -5
  8. package/modules/acl/repository/acl-action.repository.js.map +1 -1
  9. package/modules/acl/repository/acl-role.repository.d.ts +1 -5
  10. package/modules/acl/repository/acl-role.repository.js.map +1 -1
  11. package/modules/acl/services/acl-management.service.d.ts +2 -2
  12. package/modules/acl/services/acl-management.service.js +17 -20
  13. package/modules/acl/services/acl-management.service.js.map +1 -1
  14. package/modules/acl/services/acl.service.d.ts +1 -2
  15. package/modules/acl/services/acl.service.js +5 -21
  16. package/modules/acl/services/acl.service.js.map +1 -1
  17. package/modules/broker/broker.module.d.ts +2 -4
  18. package/modules/broker/broker.module.js +23 -5
  19. package/modules/broker/broker.module.js.map +1 -1
  20. package/modules/broker/config/decorator-paths.d.ts +1 -0
  21. package/modules/broker/config/decorator-paths.js +34 -4
  22. package/modules/broker/config/decorator-paths.js.map +1 -1
  23. package/modules/broker/config/route-discovery.config.d.ts +2 -0
  24. package/modules/broker/const.d.ts +1 -0
  25. package/modules/broker/const.js +2 -1
  26. package/modules/broker/const.js.map +1 -1
  27. package/modules/broker/decorators/broker-action.decorator.d.ts +2 -1
  28. package/modules/broker/decorators/broker-action.decorator.js +2 -2
  29. package/modules/broker/decorators/broker-action.decorator.js.map +1 -1
  30. package/modules/broker/services/broker.service.js +1 -1
  31. package/modules/broker/services/broker.service.js.map +1 -1
  32. package/modules/broker/services/metadata-scanner.service.js +11 -2
  33. package/modules/broker/services/metadata-scanner.service.js.map +1 -1
  34. package/modules/broker/services/route-discovery-publisher.service.js +7 -5
  35. package/modules/broker/services/route-discovery-publisher.service.js.map +1 -1
  36. package/modules/gateway-admin/config/gateway-admin.config.d.ts +4 -0
  37. package/modules/gateway-admin/const.d.ts +1 -1
  38. package/modules/gateway-admin/const.js +1 -1
  39. package/modules/gateway-admin/const.js.map +1 -1
  40. package/modules/gateway-admin/gateway-admin.module.d.ts +7 -1
  41. package/modules/gateway-admin/gateway-admin.module.js +13 -0
  42. package/modules/gateway-admin/gateway-admin.module.js.map +1 -1
  43. package/modules/gateway-admin/repository/auth-provider.repository.d.ts +3 -4
  44. package/modules/gateway-admin/repository/auth-provider.repository.js.map +1 -1
  45. package/modules/gateway-admin/services/gateway-auth.service.d.ts +3 -4
  46. package/modules/gateway-admin/services/gateway-auth.service.js +15 -23
  47. package/modules/gateway-admin/services/gateway-auth.service.js.map +1 -1
  48. package/modules/gateway-admin/services/gateway-metrics.service.d.ts +3 -0
  49. package/modules/gateway-admin/services/gateway-metrics.service.js +9 -0
  50. package/modules/gateway-admin/services/gateway-metrics.service.js.map +1 -1
  51. package/modules/gateway-admin/services/route-sync.service.d.ts +3 -1
  52. package/modules/gateway-admin/services/route-sync.service.js +14 -8
  53. package/modules/gateway-admin/services/route-sync.service.js.map +1 -1
  54. package/modules/proxy/services/http-handler.service.d.ts +3 -0
  55. package/modules/proxy/services/http-handler.service.js +27 -3
  56. package/modules/proxy/services/http-handler.service.js.map +1 -1
  57. package/package.json +5 -1
  58. package/schematics/nest-add/files/acl/src/cache/in-memory-acl-store.ts +54 -0
  59. package/schematics/nest-add/files/acl/src/modules/database/repository/acl.repository.ts +74 -0
  60. package/schematics/nest-add/files/db-core/src/modules/database/repository/in-memory-collection.ts +122 -0
  61. package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/gateway.repository.ts +151 -0
  62. package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/route-sync.repository.ts +15 -0
  63. package/schematics/nest-add/files/skills/rlb-amqp/SKILL.md +33 -5
  64. package/schematics/nest-add/files/skills/rlb-amqp/references/config-schema.md +182 -93
  65. package/schematics/nest-add/files/skills/rlb-amqp/references/gotchas.md +129 -79
  66. package/schematics/nest-add/files/skills/rlb-amqp-acl/SKILL.md +185 -0
  67. package/schematics/nest-add/files/skills/rlb-amqp-add-action/SKILL.md +87 -2
  68. package/schematics/nest-add/files/skills/rlb-amqp-add-route/SKILL.md +82 -19
  69. package/schematics/nest-add/files/skills/rlb-amqp-add-ws-event/SKILL.md +40 -18
  70. package/schematics/nest-add/files/skills/rlb-amqp-gateway-admin/SKILL.md +244 -0
  71. package/schematics/nest-add/files/skills/rlb-amqp-scaffold/SKILL.md +177 -42
  72. package/schematics/nest-add/index.js +612 -142
  73. package/schematics/nest-add/index.js.map +1 -1
  74. package/schematics/nest-add/index.ts +673 -241
  75. package/schematics/nest-add/init.schema.d.ts +10 -1
  76. package/schematics/nest-add/init.schema.ts +29 -3
  77. package/schematics/nest-add/schema.json +37 -8
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: rlb-amqp
3
- description: Reference, schema and gotchas for the @open-rlb/nestjs-amqp library (NestJS + RabbitMQ/AMQP + HTTP/WebSocket gateway). Use when answering questions about its YAML config (broker, topics, auth-providers, gateway, ws), AMQP rpc/handle/broadcast/event semantics, the @BrokerAction/@BrokerParam decorators, the BrokerService API, or when debugging wiring/timeout/auth/websocket errors. Also use as the shared knowledge base for the rlb-amqp-add-action, rlb-amqp-add-route, rlb-amqp-add-ws-event and rlb-amqp-scaffold skills.
3
+ description: Reference, schema and gotchas for the @open-rlb/nestjs-amqp library (NestJS + RabbitMQ/AMQP + HTTP/WebSocket gateway). Use when answering questions about its YAML config (broker incl. routeDiscovery, topics, auth-providers, gateway paths/ws/events), AMQP rpc/handle/broadcast/event semantics, the @BrokerAction/@BrokerParam/@BrokerHTTP/@BrokerAuth decorators, the BrokerService API, route auto-discovery, gateway-admin (routes/auth-providers/metrics/health), the name-keyed ACL, or when debugging wiring/timeout/auth/websocket errors. Shared knowledge base for the rlb-amqp-add-action, rlb-amqp-add-route, rlb-amqp-add-ws-event, rlb-amqp-scaffold, rlb-amqp-acl and rlb-amqp-gateway-admin skills.
4
4
  ---
5
5
 
6
6
  # @open-rlb/nestjs-amqp — reference
@@ -23,17 +23,32 @@ HTTP/WS → Gateway (gateway.paths / gateway.events) → topic+action → Rabbit
23
23
  topic dispatches by `action`.
24
24
  - The same method serves both **RPC** (`requestData`, waits for the reply) and **event**
25
25
  (`publishMessage`, waits only for the broker's publisher confirm).
26
+ - A microservice can announce its `@BrokerHTTP` routes to a gateway over AMQP
27
+ (**route auto-discovery**) so the gateway registers them without YAML edits.
26
28
 
27
29
  ## When to use this skill
28
30
 
29
31
  Load the bundled reference files before editing config, handlers or the gateway:
30
32
 
31
- - **`references/config-schema.md`** — full YAML schema for every section, field by field.
33
+ - **`references/config-schema.md`** — full YAML schema for every section, field by field
34
+ (broker incl. `routeDiscovery`, topics 4 modes, auth-providers name-keyed, gateway
35
+ paths + ws + events).
32
36
  - **`references/gotchas.md`** — the checklist of bug-prone cases. ALWAYS scan it before
33
37
  adding/changing a topic, queue, exchange, action, route, auth provider or WS event.
34
38
 
35
- The human-facing docs live in the repo `README.md`; these files are the terse,
36
- rules-first version for editing tasks.
39
+ The authoritative human-facing docs live under the repo `docs/` directory; these two files
40
+ are the terse, rules-first version for editing tasks:
41
+
42
+ - `docs/README.md` — index. `docs/getting-started.md` — bootstrap a ms + gateway.
43
+ - `docs/broker.md` — `@BrokerAction`/`@BrokerParam`, topic modes, RPC, `BrokerService`.
44
+ - `docs/gateway.md` — `gateway.paths[]`, `gateway.events[]`, auth gate, status mapping, ws.
45
+ - `docs/acl.md` — name-keyed actions/roles, dual grant/revoke, `acl-can-user-do*` checks.
46
+ - `docs/gateway-admin.md` — DB routes, name-keyed auth-providers, metrics, health, route-sync.
47
+ - `docs/gotchas.md` — the canonical source `references/gotchas.md` is ported from.
48
+
49
+ Runnable examples live under `sample/config-sample/` (`gateway-in-memory`, `gateway-db`,
50
+ `calculator.ms`, plus the annotated `broker/gateway/acl/gateway-admin.yaml` reference
51
+ configs). The retired `apps/gateway-2` is gone — do not cite it.
37
52
 
38
53
  ## Sibling task skills
39
54
 
@@ -41,6 +56,8 @@ rules-first version for editing tasks.
41
56
  - `rlb-amqp-add-route` — expose an action over HTTP (`gateway.paths[]`).
42
57
  - `rlb-amqp-add-ws-event` — add a secure WebSocket/webhook event (`gateway.events[]`).
43
58
  - `rlb-amqp-scaffold` — bootstrap a new microservice/gateway (module, main, config).
59
+ - `rlb-amqp-acl` — name-keyed actions/roles, grant/revoke, the `acl-can-user-do*` checks.
60
+ - `rlb-amqp-gateway-admin` — DB routes/auth-providers, metrics, health, route auto-discovery.
44
61
 
45
62
  ## Golden rules (summary — full list in references/gotchas.md)
46
63
 
@@ -49,7 +66,8 @@ rules-first version for editing tasks.
49
66
  2. `mode: rpc`/`handle` need `topics[].queue` present in `broker.queues[]`, whose
50
67
  `exchange` exists in `broker.exchanges[]`.
51
68
  3. Exchange `type: topic` → the queue MUST have a `routingKey`.
52
- 4. `broadcast` and the WebSocket gateway require `connection_name`.
69
+ 4. `broadcast` and the WebSocket gateway require a **distinct** `connection_name` per
70
+ instance (set it, or let `broker.routeDiscovery.serviceName` fill it in).
53
71
  5. `(topic, action)` must be unique — duplicates overwrite silently.
54
72
  6. No destructuring / default values in `@BrokerAction` method parameters; always pass an
55
73
  explicit `name` to `@BrokerParam`.
@@ -57,3 +75,13 @@ rules-first version for editing tasks.
57
75
  8. `roles` (HTTP or WS) require an `IAclRoleService` registered via
58
76
  `RLB_GTW_ACL_ROLE_SERVICE` in `ProxyModule.forRootAsync({ providers: [...] })`.
59
77
  Auth-providers + gateway config are passed to `ProxyModule` (not `BrokerModule`).
78
+ 9. Topic names `rlb-acl` / `rlb-gateway-admin` / `rlb-gateway-control` and ALL action
79
+ strings (`acl-*`, `gw-path-*`, `gw-auth-*`, `gw-metrics-*`, `gw-health`, `gw-reload`)
80
+ are decorator-bound and NOT configurable. Only exchange/queue/routingKey and the
81
+ route-discovery exchange/queue are.
82
+ 10. ACL actions/roles and gateway-admin auth-providers are **name-keyed**: `PUT` upserts,
83
+ `GET` lists, `GET .../get?name=`, `DELETE` by name. There is no POST and no id-based
84
+ ACL CRUD. Boolean checks (`/acl/check*`) return `200` with `true`/`false`.
85
+ 11. HTTP-route auth is **per ROUTE** and DECOUPLED from `@BrokerHTTP`: pair `@BrokerHTTP { name }`
86
+ ↔ `@BrokerAuth` `httpName`. A single `@BrokerHTTP` auto-pairs its `@BrokerAuth` (no name needed);
87
+ a route with no `@BrokerAuth` is public.
@@ -1,8 +1,19 @@
1
1
  # config.yaml — full schema
2
2
 
3
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).
4
+ Loaded by `config/config.loader.ts`. `app`/`broker`/`topics` `BrokerModule.forRoot(broker, topics, app?)`;
5
+ `auth-providers` + `gateway` `ProxyModule.forRootAsync` (`authOptions` / `gatewayOptions`).
6
+ Gateway-admin repos + the consumer-side `routeDiscovery` are wired in NEST code via
7
+ `GatewayAdminModule.forRoot/forRootAsync` — not YAML.
8
+
9
+ Authoritative sources: `docs/broker.md`, `docs/gateway.md`, `docs/gateway-admin.md`,
10
+ `docs/acl.md`, and the annotated reference YAMLs under `sample/config-sample/`
11
+ (`broker.yaml`, `gateway.yaml`, `acl.yaml`, `gateway-admin.yaml`).
12
+
13
+ > **Decorator-bound vs configurable.** Topic NAMES `rlb-acl` / `rlb-gateway-admin` /
14
+ > `rlb-gateway-control` and ALL action strings are fixed in code — write them exactly.
15
+ > `exchange` / `queue` / `routingKey` and the route-discovery `exchange` / `queue` ARE
16
+ > configurable.
6
17
 
7
18
  ---
8
19
 
@@ -24,47 +35,62 @@ to `{ message, name }`; in `development` the full detail/stack is included.
24
35
 
25
36
  ```yaml
26
37
  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
38
+ name: rabbitmq # cosmetic label (optional)
39
+ uri: "amqp://user:pass@host:5672/vhost" # string | string[] (failover); vhost after last "/"
40
+ prefetchCount: 10 # default channel prefetch
41
+ defaultRpcTimeout: 10000 # ms (call arg → topic → this → 10000)
42
+ defaultSubscribeErrorBehavior: ack # ack | nack | requeue (lib default REQUEUE)
32
43
  defaultPublishErrorBehavior: reject
33
44
 
45
+ routeDiscovery: # PUBLISHER-side route auto-discovery (microservice only)
46
+ serviceName: demo-ms # required to publish; fills connection_name if unset
47
+ publishOnBoot: true # default true — announce manifest on bootstrap
48
+ exchange: rlb-route-discovery # default; MUST match the gateway consumer
49
+ queue: rlb-route-sync # default; MUST match the gateway consumer
50
+
34
51
  connectionManagerOptions: # amqp-connection-manager options
35
52
  heartbeatIntervalInSeconds: 60
36
53
  reconnectTimeInSeconds: 60
37
54
  connectionOptions:
38
55
  clientProperties:
39
- connection_name: my-service # REQUIRED for broadcast + WebSocket gateway
56
+ connection_name: my-service-1 # DISTINCT per instance for broadcast + WebSocket
40
57
  credentials:
41
58
  mechanism: PLAIN # PLAIN | EXTERNAL | AMQPLAIN (case-insensitive)
42
59
  username: guest
43
60
  password: guest
44
61
 
62
+ connectionInitOptions: # block on a healthy connection at boot?
63
+ wait: true # default true
64
+ timeout: 5000 # default 5000 ms
65
+ reject: true # default true → throw on timeout
66
+
45
67
  exchanges: # RabbitMQExchangeConfig[]
46
- - name: users-ex
68
+ - name: rlb
47
69
  type: direct # direct | topic | fanout | headers
48
70
  createExchangeIfNotExists: true # false → checkExchange (must pre-exist)
49
71
  options: { durable: true, autoDelete: false, internal: false }
50
72
 
51
73
  queues: # RabbitMQQueueConfig[]
52
- - name: users-rpc-q
53
- exchange: users-ex
54
- routingKey: users.rpc # string | string[]; REQUIRED if exchange type == topic
74
+ - name: rlb-acl
75
+ exchange: rlb
76
+ routingKey: rlb-acl # string | string[]; REQUIRED if exchange type == topic
55
77
  createQueueIfNotExists: true
56
78
  options: { durable: true, exclusive: false, autoDelete: false }
57
- consumerTag: my-tag # optional, must be unique per channel
79
+ consumerTag: my-tag # optional, unique per channel
58
80
 
59
81
  replyQueues: # map exchange → reply queue (RPC responses)
60
- users-ex: users-reply-q # omit → RabbitMQ direct-reply-to is used
82
+ rlb: rlb-reply # omit → RabbitMQ direct-reply-to is used
61
83
  ```
62
84
 
63
85
  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, ...).
86
+ - `exchanges[]` / `queues[]` are asserted/checked once at boot; `replyQueues` values auto-consumed.
87
+ - `routeDiscovery.serviceName` doubles as `connection_name` when no explicit `clientProperties.connection_name`
88
+ is set (explicit always wins). The **gateway** (consumer) does NOT use `broker.routeDiscovery`
89
+ it sets its side via `GatewayAdminModule` `routeDiscovery { exchange, queue }`; both sides must match.
90
+ - Do NOT declare a `broadcast` topic's per-instance queue here — the broker asserts
91
+ `${topic}-${connection_name}` for you at subscribe time.
92
+ - Other optional fields: `defaultAlternateExchange` (divert unroutable), `onUnroutableMessage`
93
+ (callback, needs `mandatory: true` publishes), per-channel `channels` map.
68
94
 
69
95
  ---
70
96
 
@@ -74,60 +100,70 @@ A topic maps a logical name to an AMQP path. `mode` decides the semantics.
74
100
 
75
101
  ```yaml
76
102
  topics:
77
- - name: users-rpc # logical name (must match @BrokerAction / requestData / gateway)
103
+ - name: rlb-acl # logical name (must match @BrokerAction / requestData / gateway)
78
104
  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
105
+ queue: rlb-acl # for rpc/handle: must exist in broker.queues[]
106
+ exchange: rlb # exchange name
107
+ routingKey: rlb-acl # broadcast / topic exchanges
108
+ errorBehavior: ack # per-topic override of defaultSubscribeErrorBehavior (default REQUEUE)
109
+ mandatory: false # publish with AMQP `mandatory` (unroutable → returned)
110
+ persistent: false # publish delivery-mode 2 (survives restart if queue durable)
82
111
  toObservable: false # handle only: route to BrokerService.events$ instead of a handler
83
112
  ```
84
113
 
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 |
114
+ | mode | required fields | notes |
115
+ | ----------- | ------------------------------------------------ | ------------------------------------------------------------------ |
116
+ | `rpc` | `name`, `queue` (or `exchange`) | request/response + timeout; also the mode `@BrokerAction` uses |
117
+ | `handle` | `name`, `queue` | plain consumer, no reply (`registerHandler` / `toObservable`) |
118
+ | `broadcast` | `name`, `exchange`, `routingKey` | per-instance queue `${topic}-${connection_name}`; distinct name |
119
+ | `event` | `name`, `exchange`+`routingKey` (or `queue`) | fire-and-forget publish; no consumer asserted for the topic |
91
120
 
92
121
  > A single `@BrokerAction` topic registers ONE consumer; multiple actions on the same
93
- > topic share it and are dispatched by `action`.
122
+ > topic share it and are dispatched by `action`. The gateway control topic
123
+ > (`rlb-gateway-control`) is `broadcast` so every instance reloads.
94
124
 
95
125
  ---
96
126
 
97
127
  ## auth-providers (HandlerAuthConfig[])
98
128
 
129
+ Top-level (NOT under `gateway`). Each provider VALIDATES a token and MAPS its claims into
130
+ forwarded `X-GTW-AUTH-*` headers. Referenced by `paths[].auth` / `events[].auth` by `name`.
131
+
99
132
  ```yaml
100
133
  auth-providers:
101
134
  - name: gateway-jwks
102
135
  type: jwks # jwt | jwks | basic | str-compare | none
103
- issuer: https://issuer/realms/x
136
+ headerPrefix: "X-GTW-AUTH-" # prefix for mapped claim headers (and <prefix>USERID)
137
+ uidClaim: sub # claim → <prefix>USERID; REQUIRED for role checks
138
+ jwtMap: # 'source:dest' pairs → <prefix><DEST>; WITHOUT it NO claims forwarded
139
+ - sub:userId # → X-GTW-AUTH-USERID
140
+ - email:email # → X-GTW-AUTH-EMAIL
141
+ - preferred_username:username # → X-GTW-AUTH-USERNAME
142
+ - roles:roles # → X-GTW-AUTH-ROLES
143
+ algorithms: [RS256] # REQUIRED for jwt/jwks; jwks allows only RS*/ES*/PS*
144
+ issuer: https://issuer/realms/x # expected `iss`
104
145
  jwksUri: https://issuer/certs # jwks only
105
- secret: s3cr3t # jwt / str-compare only
106
- audience: my-aud # jwt only
107
- algorithms: [RS256]
146
+ secret: s3cr3t # jwt (HS secret) / str-compare (expected token string)
147
+ audience: my-aud # jwt only (optional)
148
+ clientId: u # basic only (username)
149
+ clientSecret: p # basic only (password)
108
150
  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 / aclAction are DEPRECATED & unused: the gateway role check runs in-process
116
- # (IAclRoleService.canUserDoGtw) — no RPC topic/action needed.
117
151
  ```
118
152
 
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')`.
153
+ Type behaviour:
154
+ - **`jwt` / `jwks`** verify `Authorization: Bearer <token>`, then map via `jwtMap`.
155
+ - **`basic`** — verify `Authorization: Basic` vs `clientId`/`clientSecret`; maps username to
156
+ `<prefix>USERNAME`/`<prefix>USERID`. **No `clientSecret` → passes through** (open by design).
157
+ - **`str-compare`** — compare raw `Authorization` to `secret`; maps to `<prefix>TOKEN`.
158
+ **No `secret` → passes through.**
122
159
 
123
- Types: `jwt` (HS/RS secret), `jwks` (remote keys), `basic` (clientId/clientSecret),
124
- `str-compare` (static token after `headerPrefix` in Authorization).
160
+ Hardening: `algorithms` REQUIRED for `jwt`/`jwks` (omit denied; algorithm-confusion guard).
161
+ Define `jwtMap` or no identity is forwarded (token still `success:true` fail-safe, not leak).
162
+ `usernameClaim` is deprecated; `aclTopic`/`aclAction` are removed (gateway role check is in-process
163
+ via `IAclRoleService.canUserDoGtw`).
125
164
 
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 forward identity headers: without it NO claims are forwarded (the token is
130
- still accepted, `success:true`) — fail-safe instead of leaking the whole payload.
165
+ > DB-stored auth-providers (name-keyed `gw-auth-*` upserts) layer on top of this static list
166
+ > see `docs/gateway-admin.md`.
131
167
 
132
168
  ---
133
169
 
@@ -136,80 +172,133 @@ still accepted, `success:true`) — fail-safe instead of leaking the whole paylo
136
172
  ```yaml
137
173
  gateway:
138
174
  mode: gateway
139
- headerPrefix: X-GTW- # prefix for forwarded request headers (forwardHeaders)
175
+ headerPrefix: "X-FWD-" # prefix for FORWARDED request headers (forwardHeaders);
176
+ # separate from a provider's headerPrefix (auth claims)
177
+
178
+ reloadTopic: rlb-gateway-control # broadcast control topic; action 'gw-reload' rebuilds routes
179
+ metrics: # per-call broker sink (omit to disable)
180
+ topic: rlb-gateway-admin
181
+ action: gw-metrics-track
182
+
183
+ loadConfig: # pull DB-stored routes/events, merged with YAML, on (re)load
184
+ paths: { topic: rlb-gateway-admin, action: gw-path-export } # gateway-admin ships this handler
185
+ # events: { topic: <topic>, action: <your-export-action> } # optional; NO built-in handler — provide your own
140
186
 
141
187
  ws: # WebSocketGatewayOptions — connection-level only
142
- maxConnections: 5000
188
+ maxConnections: 1000
143
189
  maxSubscriptionsPerClient: 50
144
- heartbeatIntervalMs: 30000
145
- allowedOrigins: [https://app.example.com] # Origin allowlist (omit all accepted)
146
- maxMessageBytes: 16384 # drop oversized client frames (default 16KB)
190
+ heartbeatIntervalMs: 30000 # default 30000; also drops dead/expired-token sockets
191
+ maxMessageBytes: 16384 # default 16384; oversized client frames dropped
192
+ allowedOrigins: [https://app.example.com] # omit all Origins accepted (logged)
147
193
  # auth/roles/scope are declared PER-EVENT on events[], not here
148
194
 
149
- loadConfig: # optional remote load via RPC at boot
150
- paths: { topic: gtw.config, action: get-paths }
151
- events: { topic: gtw.config, action: get-events }
152
-
153
195
  paths: [ ... ] # PathDefinition[] (HTTP routes) — see below
154
196
  events: [ ... ] # WebSocketEvent[] (WS / webhook) — see below
155
197
  ```
156
198
 
199
+ The reload control action is the literal string **`gw-reload`** (`GW_RELOAD_ACTION`); the
200
+ control-topic subscriber ignores every other message. Concurrent `reload()`s are coalesced.
201
+
157
202
  ### gateway.paths[] (PathDefinition — HTTP routes)
158
203
 
159
204
  ```yaml
160
- - name: users-create
161
- method: POST # GET | POST | PUT | DELETE | PATCH
162
- path: /users/:tenant? # Express route, params supported
163
- dataSource: body # body | query | params | body-query | query-body
164
- topic: users-rpc
165
- action: user.create
166
- mode: rpc # rpc | event
167
- timeout: 7000 # rpc only (ms)
168
- auth: gateway-jwks # auth-provider name
169
- allowAnonymous: false # true → allow even without valid auth
170
- roles: [users.create] # requires IAclRoleService
171
- successStatusCode: 201
172
- binary: false # true response sent as base64-decoded Buffer
173
- redirect: 302 # if set → redirect to the URL contained in the reply
174
- parseRaw: false # true → forward raw body as $raw (needs rawBody:true in bootstrap)
205
+ - name: report-download # logical name (logs + metrics)
206
+ method: GET # GET | POST | PUT | DELETE | PATCH
207
+ path: /reports/:id # Express route, :params merged into payload (params win)
208
+ dataSource: query-body # body | query | params | body-query | query-body
209
+ topic: rlb-gateway-admin
210
+ action: gw-metrics-get
211
+ mode: rpc # rpc | event
212
+ timeout: 15000 # rpc only (ms)
213
+ auth: gateway-jwks # auth-provider name
214
+ allowAnonymous: false # true → skip the auth/role gate entirely
215
+ roles: [user, admin] # caller must hold AT LEAST ONE; requires auth + IAclRoleService
216
+ successStatusCode: 200 # default 200 rpc / 202 event / 204 empty rpc reply
217
+ binary: true # treat a raw (non-JSON) reply as base64 → binary body
218
+ redirect: 302 # rpc only → redirect with this status, using the reply as Location
219
+ parseRaw: false # true → forward raw body as $raw (needs rawBody:true at bootstrap)
175
220
  headers: { Cache-Control: no-store } # static response headers
176
- forwardHeaders: { Tenant: x-tenant } # request header → forwarded to the microservice
221
+ forwardHeaders: { X-Trace-Id: X-Request-Id } # request header → forwarded (dest prefixed by headerPrefix)
177
222
  ```
178
223
 
179
- dataSource payload composition:
224
+ dataSource payload composition (`req.params` always merged in, re-applied last so they win):
180
225
 
181
226
  | value | payload |
182
227
  | ------------ | -------------------------------- |
183
228
  | `body` | `{...params, ...body}` |
184
229
  | `query` | `{...params, ...query}` |
185
- | `params` | `params` |
230
+ | `params` | `{...params}` |
186
231
  | `body-query` | `{...params, ...query, ...body}` |
187
232
  | `query-body` | `{...params, ...body, ...query}` |
188
233
 
189
- Route params are re-applied last (win on key collisions). Uploads → `$files`, raw → `$raw`.
190
- Error `name` → HTTP status: BadRequestError/InvalidParamsErrror→400, UnauthorizedError→401,
191
- ForbiddenError→403, NotFoundError→404, else→500. `mode: event` confirm failure → 503.
234
+ Uploads `$files` (buffers as binary strings), raw → `$raw`.
235
+
236
+ **rpc status:** defined reply (incl. falsy `false`/`0`/`""`) `200` + body; `null`/`undefined`
237
+ → `204`. Error `name` → status: BadRequestError/InvalidParamsErrror→400, UnauthorizedError→401,
238
+ ForbiddenError→403, NotFoundError→404, ConflictError→409, else→500.
239
+ **event status:** successful publish → `successStatusCode || 202`; publish failure → `503`.
240
+
241
+ Auth gate (per request): `allowAnonymous:true` → gate skipped; `auth` no `roles` → authn only
242
+ (401 if invalid); `auth` + `roles` → authn then in-process `canUserDoGtw(roles, userId)` (403 if
243
+ no role). `roles` without `auth` fails closed (every request 403).
192
244
 
193
245
  ### gateway.events[] (WebSocketEvent — WS / webhook)
194
246
 
195
247
  ```yaml
196
- - name: orders
197
- type: ws # ws | http (webhook)
198
- exchange: orders-ex
199
- routingKey: orders.#
248
+ - name: chat # clients subscribe to "chat"; messages arrive as onChat
249
+ type: ws # ws | mqtt (reserved) | http (webhook)
250
+ exchange: rlb # broker source bound to a per-instance exclusive ephemeral queue
251
+ routingKey: chat.messages
200
252
  auth: gateway-jwks # provider that verifies the token + maps claims FOR THIS event (at subscribe)
201
253
  requireAuth: true # default true when `auth` is set; false → auth optional (anon allowed)
202
- roles: [orders.read] # ACL check via IAclRoleService
203
- scopeClaim: X-GTW-AUTH-USERID # forward only messages of this user...
204
- payloadKey: userId # ...where payload.userId === the mapped claim value
254
+ roles: [user] # ACL check via IAclRoleService (needs auth)
255
+ scopeClaim: userId # per-user isolation: the mapped claim value...
256
+ payloadKey: userId # ...must equal payload[payloadKey]; without payloadKey denies everything
205
257
  # type: http only:
206
258
  url: https://hooks.example.com/orders
207
259
  method: POST
208
- timeout: 8000
260
+ timeout: 5000
209
261
  headers: { Authorization: "Bearer ..." }
210
262
  ```
211
263
 
212
- WS client connects with the JWT in the subprotocol: `new WebSocket(url, [token])`. The token
213
- is verified per-event with `events[].auth`'s provider (memoized per provider per connection).
214
- Client protocol: `{action:'subscribe'|'unsubscribe', topic, select?}`; inbound messages
215
- arrive as `{ topic: 'on<Name>', data }`, errors as `{ topic:'onError', data:{event,error} }`.
264
+ WS client connects with the JWT in the subprotocol: `new WebSocket(url, [token])` (browsers
265
+ can't set handshake headers). A bare value, or `['bearer', '<token>']` / `['jwt', '<token>']`,
266
+ is accepted. Token verified per-event at subscribe (memoized per provider per connection);
267
+ session bounded by the JWT `exp` (closed `1008` on expiry). Client protocol:
268
+ `{action:'subscribe'|'unsubscribe', topic, select?}`; inbound `{ topic:'on<Name>', data }`;
269
+ errors `{ topic:'onError', data:{event,error} }` (`unauthorized`, `forbidden`,
270
+ `subscription_limit`, `unknown_event`). `select` is a client filter, intersected with — never
271
+ bypassing — the server `scopeClaim` isolation. Each instance binds its own exclusive
272
+ auto-delete queue, so every instance needs a distinct `connection_name`.
273
+
274
+ ---
275
+
276
+ ## gateway-admin / ACL HTTP surface (decorator-bound topics + actions)
277
+
278
+ These are exposed as ordinary `gateway.paths[]` entries forwarding to the fixed topics/actions
279
+ below. **Name-keyed** resources have NO POST and no id — `PUT` upserts by `name`.
280
+
281
+ ACL (topic `rlb-acl`):
282
+
283
+ | Method | Path | Action |
284
+ | --- | --- | --- |
285
+ | GET | `/acl/check` | `acl-can-user-do-gtw` (resource-agnostic; `200` true/false) |
286
+ | GET | `/acl/check-resource` | `acl-can-user-do` (resource-scoped; `200` true/false) |
287
+ | GET | `/acl/resources` | `acl-list-resources-by-user` (auth, reads `X-GTW-AUTH-USERID`) |
288
+ | POST | `/acl/grants` | `acl-grant` (`{userId, roles, resourceId?, companyId?}`) |
289
+ | DELETE | `/acl/grants` | `acl-revoke` (same shape; `roles` REQUIRED) |
290
+ | GET / PUT / DELETE | `/acl/actions[/get?name=]` | `acl-action-list`/`-update`/`-delete`/`-get` |
291
+ | GET / PUT / DELETE | `/acl/roles[/get?name=]` | `acl-role-list`/`-update`/`-delete`/`-get` |
292
+
293
+ Gateway-admin (topic `rlb-gateway-admin`, except reload):
294
+
295
+ | Method | Path | Action |
296
+ | --- | --- | --- |
297
+ | GET | `/health` | `gw-health` → `{ status: 'ok' }` |
298
+ | POST / GET / PUT / DELETE | `/admin/paths[/export\|/get]` | `gw-path-create`/`-list`/`-export`/`-update`/`-get`/`-delete` (**id-keyed, POST creates**) |
299
+ | GET / PUT / DELETE | `/admin/auth[/get?name=]` | `gw-auth-list`/`-update`/`-delete`/`-get` (**name-keyed PUT-upsert**) |
300
+ | GET | `/admin/metrics[/series\|/points]` | `gw-metrics-get`/`-series`/`-points` |
301
+ | POST | `/admin/metrics/track` | `gw-metrics-track` (`mode: event`) |
302
+ | POST | `/admin/reload` | `gw-reload` on `rlb-gateway-control` (`mode: event`) |
303
+
304
+ > Removed: `acl-list-by-user`, `acl-verify-access`, `gw-auth-create`, all id-based ACL CRUD.