@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
@@ -7,22 +7,46 @@ description: Expose a broker action over HTTP through the @open-rlb/nestjs-amqp
7
7
 
8
8
  Read first:
9
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)
10
+ - `.claude/skills/rlb-amqp/references/gotchas.md` (HTTP + auth items)
11
+ - `docs/gateway.md` is the authority for the `PathDefinition` fields and status mapping.
11
12
 
12
13
  The target `topic`+`action` should already have a handler (otherwise also run
13
14
  `rlb-amqp-add-action`). The route only needs the topic to exist in `topics[]`.
15
+ Canonical example: `sample/config-sample/gateway-in-memory/config/config.yaml`.
14
16
 
15
17
  ## Decide
16
18
 
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.
19
+ - **mode**: `rpc` (return the handler's reply) or `event` (publish-and-confirm, no reply).
20
+ - **dataSource**: how the payload is assembled — `req.params` are ALWAYS merged in, plus:
21
+ `body` | `query` | `params` | `body-query` (body wins) | `query-body` (query wins).
22
+ - **auth**: an `auth-provider` name (validates the request, maps claims to `X-GTW-AUTH-*`
23
+ headers). `allowAnonymous: true` skips the gate. `roles: [...]` adds a role check.
23
24
  - Extras: `timeout` (rpc), `successStatusCode`, `binary`, `redirect`, `parseRaw`, static
24
25
  `headers`, `forwardHeaders`.
25
26
 
27
+ ## PathDefinition fields (all of them)
28
+
29
+ | Field | Notes |
30
+ | --- | --- |
31
+ | `name` | Unique; used in logs + metrics. |
32
+ | `method` | `GET` `POST` `PUT` `DELETE` `PATCH` … |
33
+ | `path` | Express route, may carry `:params` (e.g. `/users/:id`). |
34
+ | `topic` / `action` | Broker destination. Action strings are decorator-bound on the backend. |
35
+ | `mode` | `rpc` \| `event`. |
36
+ | `dataSource` | `body` \| `query` \| `params` \| `body-query` \| `query-body`. |
37
+ | `auth` | Auth-provider name; validates + maps claims. |
38
+ | `allowAnonymous` | `true` → gate skipped (token still mapped if present & valid). |
39
+ | `roles` | Role NAMES; caller passes with AT LEAST ONE. Requires `auth`. |
40
+ | `timeout` | RPC timeout (ms), `rpc` only. |
41
+ | `binary` | Treat a raw (non-JSON) RPC reply as base64 → binary body. |
42
+ | `parseRaw` | Adds the raw request body as `$raw` (needs `rawBody: true`). |
43
+ | `successStatusCode` | Override success status (default 200 rpc / 202 event / 204 empty). |
44
+ | `redirect` | On an `rpc` route, redirect with this status using the reply as the location. |
45
+ | `headers` | Static response headers `{ k: v }`. |
46
+ | `forwardHeaders` | `{ dest: srcHeader }` — copy request headers downstream (prefixed by `gateway.headerPrefix`). |
47
+
48
+ Uploaded multipart files (any field) are attached as `$files` (buffers → binary strings).
49
+
26
50
  ## YAML fragment
27
51
 
28
52
  ```yaml
@@ -36,28 +60,67 @@ gateway:
36
60
  action: <action>
37
61
  mode: rpc # or event
38
62
  auth: gateway-jwks # optional
39
- roles: [resource.write] # optional → needs IAclRoleService
63
+ roles: [resource.write] # optional → needs RLB_GTW_ACL_ROLE_SERVICE
40
64
  timeout: 7000 # rpc only
41
65
  successStatusCode: 201
42
66
  ```
43
67
 
68
+ ## The 3-case auth gate
69
+
70
+ For every request the gateway runs `processAuthData` (best-effort), then:
71
+
72
+ 1. **`allowAnonymous: true`** → gate SKIPPED. A valid token still gets its claims mapped &
73
+ forwarded; a missing/invalid token is NOT blocked.
74
+ 2. **`auth` set, no `roles`** → authentication only. Provider must validate (else `401`);
75
+ on success the `X-GTW-AUTH-*` headers are forwarded downstream.
76
+ 3. **`auth` + `roles`** → authn + role authz. After a valid token the gateway reads the user
77
+ id from the provider's `uidClaim` and calls `IAclRoleService.canUserDoGtw(roles, userId)`
78
+ in-process. Passes with at least one role, else `403`.
79
+
80
+ > `roles` WITHOUT `auth` is a misconfiguration: no identity → fails closed (every request
81
+ > `403`, logged loudly at boot). The resource-scoped check (`acl-can-user-do`) is NOT run by
82
+ > the gateway — it lives on the target microservice.
83
+
84
+ ## Status mapping
85
+
86
+ **`rpc`** reply → status:
87
+
88
+ | Reply | Status |
89
+ | --- | --- |
90
+ | Defined value (incl. falsy `false` / `0` / `''`) | `200` + body |
91
+ | `null` / `undefined` | `204 No Content` |
92
+
93
+ > ONLY `null`/`undefined` collapses to `204`. A defined falsy result is real content, so a
94
+ > boolean check route answers `200` with body `false` — not an empty `204`.
95
+
96
+ **`rpc`** error → status (by error `name`):
97
+
98
+ | Error name | Status |
99
+ | --- | --- |
100
+ | `BadRequestError`, `InvalidParamsErrror` | `400` |
101
+ | `UnauthorizedError` | `401` |
102
+ | `ForbiddenError` | `403` |
103
+ | `NotFoundError` | `404` |
104
+ | `ConflictError` | `409` |
105
+ | (any other) | `500` |
106
+
107
+ **`event`** route: successful publish → `successStatusCode || 202`; publish failure → `503`.
108
+
44
109
  ## Required wiring to flag
45
110
 
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: [...] })`. The check
50
- is role-based (`canUserDoGtw(path.roles, userId)`): `roles` are ROLE NAMES and the user
51
- passes with AT LEAST ONE. The auth-provider only needs `uidClaim` (+ `headerPrefix`) to
52
- extract the userId (gotcha 15). For a resource-scoped decision, the target ms calls
53
- `canUserDo(roles, userId, resourceId)` (RPC `acl-can-user-do`) itself.
111
+ - If `parseRaw: true` → bootstrap with `NestFactory.create(AppModule, { rawBody: true })`.
112
+ - If `roles` is used an `IAclRoleService` must be registered via `RLB_GTW_ACL_ROLE_SERVICE`
113
+ in `ProxyModule.forRootAsync({ providers: [{ provide: RLB_GTW_ACL_ROLE_SERVICE, useExisting: AclService }] })`.
114
+ If a path declares `roles` and the service is NOT registered → request DENIED (`403`) +
115
+ error logged. The auth-provider needs a `uidClaim` (+ `headerPrefix`) to resolve the userId.
54
116
  - Forwarded auth claims reach the handler as prefixed/uppercased headers
55
- (e.g. `X-GTW-AUTH-USERID`) — read them with `@BrokerParam('header', ...)`.
117
+ (e.g. `X-GTW-AUTH-USERID`) — read them with `@BrokerParam('header', ...)`. Request headers
118
+ can never override mapped claim headers (anti-spoofing).
56
119
 
57
120
  ## Verify
58
121
 
59
- - topic exists in `topics[]` and resolves (gotchas 5–7).
60
- - route-param vs body/query key collisions are intentional (gotcha 13).
122
+ - topic exists in `topics[]` and resolves.
123
+ - route-param vs body/query key collisions are intentional (params always merge in).
61
124
  - `npm run build`, then optionally curl the route once the broker is up.
62
125
 
63
126
  Output the YAML fragment (with parent path), plus any bootstrap/ACL action the user still
@@ -8,38 +8,47 @@ description: Add a secure WebSocket event (or HTTP webhook) to the @open-rlb/nes
8
8
  Read first:
9
9
  - `.claude/skills/rlb-amqp/references/config-schema.md` (the `gateway.events[]` + `gateway.ws`
10
10
  sections)
11
- - `.claude/skills/rlb-amqp/references/gotchas.md` (WebSocket items 16–18, ACL item 15)
11
+ - `.claude/skills/rlb-amqp/references/gotchas.md` (WebSocket items 16–18, 26–27; ACL item 15)
12
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.
13
+ A WS event binds a broker `exchange`/`routingKey` to a named client-facing stream and fans
14
+ each message out to the connected clients of EVERY gateway instance. Secure it by default.
15
15
 
16
16
  ## Decide
17
17
 
18
18
  - **type**: `ws` (push to clients) or `http` (forward each message to a webhook `url`).
19
+ (`mqtt` is also accepted by the type union but `ws`/`http` are the supported paths.)
19
20
  - **source**: `exchange` + `routingKey` (the exchange must exist in `broker.exchanges[]`;
20
- `connection_name` must be set — gotcha 8).
21
+ each instance needs a distinct `connection_name` — gotcha 8).
22
+ - **client-facing name**: clients subscribe by `name`; messages arrive as `on<Name>`
23
+ (first letter capitalized — `chat` → `onChat`).
21
24
  - **security**:
22
25
  - `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.
26
+ for THIS event (at subscribe time, memoized per provider). When set, a valid token is
27
+ required to subscribe.
24
28
  - `requireAuth: false` → makes `auth` optional (anonymous allowed; claims mapped if a token
25
29
  is present — handy with `scopeClaim`). Defaults to `true` when `auth` is set.
26
30
  - `roles: [...]` → ACL check (needs `IAclRoleService`); requires `auth` for the identity.
27
- - `scopeClaim` + `payloadKey` → per-user scoping: a client only receives messages where
31
+ - `scopeClaim` + `payloadKey` → per-user isolation: a client only receives messages where
28
32
  `payload[payloadKey] === claims[scopeClaim]`. `scopeClaim` is the MAPPED claim
29
33
  (with `headerPrefix`, e.g. `X-GTW-AUTH-USERID`). Without `payloadKey` it denies all
30
- (gotcha 16).
34
+ (gotcha 16). With `auth` but no `scopeClaim`/`payloadKey`, every authorized subscriber
35
+ gets ALL messages (warned at boot).
31
36
 
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.
37
+ > Auth/roles/scope are declared PER-EVENT. `gateway.ws` only holds connection-level limits,
38
+ > heartbeat, origin allowlist and message-size cap (no auth fields). Different events may use
39
+ > different providers.
34
40
 
35
41
  ## YAML fragments
36
42
 
37
43
  ```yaml
38
44
  gateway:
39
45
  ws: # connection-level only (optional)
40
- maxConnections: 5000
41
- maxSubscriptionsPerClient: 50
42
- heartbeatIntervalMs: 30000
46
+ maxConnections: 5000 # max concurrent connections this instance accepts
47
+ maxSubscriptionsPerClient: 50 # max active subscriptions per client
48
+ heartbeatIntervalMs: 30000 # ping/pong; also drops dead sockets + expired tokens
49
+ allowedOrigins: # reject cross-site handshakes; omit → all origins (logged)
50
+ - https://app.example.com
51
+ maxMessageBytes: 16384 # inbound client frame cap; larger frames dropped (default)
43
52
 
44
53
  events:
45
54
  - name: orders
@@ -49,8 +58,8 @@ gateway:
49
58
  auth: gateway-jwks # verifies token + maps claims for this event
50
59
  requireAuth: true # default true when auth is set; false → optional
51
60
  roles: [orders.read] # optional → needs IAclRoleService
52
- scopeClaim: X-GTW-AUTH-USERID # optional per-user scoping
53
- payloadKey: userId
61
+ scopeClaim: X-GTW-AUTH-USERID # optional per-user scoping (MAPPED claim)
62
+ payloadKey: userId # message field compared to scopeClaim
54
63
 
55
64
  - name: invoices # webhook variant
56
65
  type: http
@@ -58,6 +67,7 @@ gateway:
58
67
  routingKey: inv.#
59
68
  url: https://hooks.example.com/invoices
60
69
  method: POST
70
+ headers: { x-api-key: secret }
61
71
  timeout: 8000
62
72
  ```
63
73
 
@@ -75,19 +85,31 @@ broker:
75
85
  ## Required wiring to flag
76
86
 
77
87
  - The app bootstrap must register the WS adapter:
78
- `app.useWebSocketAdapter(new WsAdapter(app))`.
88
+ `app.useWebSocketAdapter(new WsAdapter(app))` (see
89
+ `sample/config-sample/gateway-in-memory/src/main.ts`).
79
90
  - `events[].auth` must reference a `jwt`/`jwks` provider; subscribing without a valid token
80
91
  yields `{ topic:'onError', data:{ event, error:'unauthorized' } }` (unless `requireAuth:false`).
81
- - `roles` `IAclRoleService` via `RLB_GTW_ACL_ROLE_SERVICE` (gotcha 15).
92
+ A failed role check yields `error:'forbidden'`.
93
+ - `roles` → `IAclRoleService` via `RLB_GTW_ACL_ROLE_SERVICE` in
94
+ `ProxyModule.forRootAsync({ providers: [...] })` (gotcha 15).
82
95
  - Do NOT add a fixed durable queue for the event — the lib creates a per-instance exclusive
83
- queue for fan-out (gotcha 17).
96
+ ephemeral auto-delete queue for fan-out (gotcha 17).
97
+ - WS sessions are bounded by the token `exp`: the socket is closed (`1008`) when the JWT
98
+ expires; long-lived sockets need refresh + reconnect (gotcha 26).
84
99
 
85
100
  ## Client snippet (for docs/testing)
86
101
 
87
102
  ```js
88
- const ws = new WebSocket('ws://localhost:3000', [token]); // token in subprotocol
103
+ // token in subprotocol — browsers can't set custom handshake headers (gotcha 18).
104
+ // single value = token; ['bearer', token] / ['jwt', token] pairs also accepted.
105
+ const ws = new WebSocket('ws://localhost:3000', [token]);
89
106
  ws.onopen = () => ws.send(JSON.stringify({ action: 'subscribe', topic: 'orders' }));
90
107
  ws.onmessage = (e) => console.log(JSON.parse(e.data)); // { topic:'onOrders', data } | { topic:'onError', ... }
108
+ // unsubscribe: ws.send(JSON.stringify({ action: 'unsubscribe', topic: 'orders' }));
91
109
  ```
92
110
 
111
+ > An optional `select: { key: value }` in the subscribe frame is a client-side filter
112
+ > (forwarded only when `payload[key] === value` for every key). It is INTERSECTED with the
113
+ > server-enforced `scopeClaim` isolation — a `select` can never widen what a user receives.
114
+
93
115
  Output the YAML fragments (with parent paths) and the bootstrap/ACL items still required.
@@ -0,0 +1,244 @@
1
+ ---
2
+ name: rlb-amqp-gateway-admin
3
+ description: Drive the @open-rlb/nestjs-amqp gateway-admin management plane and route auto-discovery. Use for DB-managed HTTP routes (gw-path-*), DB auth-providers CRUD (gw-auth-*, name-keyed PUT-upsert), gateway metrics (gw-metrics-get/series/points/track) and the gw-health probe, runtime route reload (gw-reload via the broadcast control topic), the in-proxy metrics hook (RLB_GTW_METRICS_HOOK), and wiring GatewayAdminModule.forRoot/forRootAsync. Also covers route auto-discovery: publisher (broker.routeDiscovery serviceName/publishOnBoot/exchange/queue) vs consumer (GatewayAdminModule routeDiscovery exchange/queue).
4
+ ---
5
+
6
+ # Gateway-admin module + route auto-discovery
7
+
8
+ Read first:
9
+ - `docs/gateway-admin.md` (full reference)
10
+ - `sample/config-sample/gateway-admin.yaml` (annotated YAML for every action)
11
+ - `sample/config-sample/gateway-db/` (runnable wiring; `apps/gateway-2` was retired)
12
+
13
+ The gateway-admin module is the gateway's management plane: DB-stored routes,
14
+ DB auth-providers, metrics, runtime reload, and the consumer side of route
15
+ auto-discovery. All handlers bind to the topic `rlb-gateway-admin`
16
+ (`GATEWAY_ADMIN_TOPIC`). The topic NAME and every action string are
17
+ **decorator-bound — NOT configurable**. You drive them by adding
18
+ `gateway.paths[]` that forward to that topic/action.
19
+
20
+ ## Fixed vs configurable
21
+
22
+ - FIXED (write exactly): topic names `rlb-gateway-admin`, control action
23
+ `gw-reload`, and all action strings `gw-path-*` / `gw-auth-*` / `gw-metrics-*`
24
+ / `gw-health` (from `GW_ADMIN_ACTIONS` + `GW_RELOAD_ACTION`).
25
+ - CONFIGURABLE: each topic's exchange/queue/routingKey, and the route-discovery
26
+ exchange/queue (defaults `rlb-route-discovery` / `rlb-route-sync`) — which must
27
+ match on the publisher AND consumer sides.
28
+
29
+ ## Nest wiring — `GatewayAdminModule`
30
+
31
+ `forRoot(providers, options)`: repo bindings FIRST, options SECOND. You supply
32
+ the four repositories (any store); the module ships the services and wires
33
+ `RouteSyncService` internally. Tokens are re-exported from `@open-rlb/nestjs-amqp`.
34
+
35
+ ```ts
36
+ GatewayAdminModule.forRoot([
37
+ { provide: HttpPathRepository, useExisting: MongoHttpPathRepository },
38
+ { provide: AuthProviderRepository, useExisting: MongoAuthProviderRepository },
39
+ { provide: HttpMetricRepository, useExisting: MongoHttpMetricRepository },
40
+ { provide: RouteSyncLogRepository, useExisting: MongoRouteSyncLogRepository }, // route-discovery journal
41
+ ]),
42
+ ```
43
+
44
+ Use `forRootAsync` to resolve the **consumer-side** `routeDiscovery { exchange, queue }`
45
+ from config (see Route auto-discovery). Exports `GatewayPathService`,
46
+ `GatewayAuthService`, `GatewayMetricsService`.
47
+
48
+ ## Broker topic + queue (required)
49
+
50
+ ```yaml
51
+ broker:
52
+ queues:
53
+ - name: rlb-gateway-admin
54
+ exchange: rlb
55
+ routingKey: rlb-gateway-admin
56
+ createQueueIfNotExists: true
57
+ options: { durable: true }
58
+ topics:
59
+ - name: rlb-gateway-admin # MUST be this name
60
+ mode: rpc
61
+ queue: rlb-gateway-admin
62
+ exchange: rlb
63
+ routingKey: rlb-gateway-admin
64
+ - name: rlb-gateway-control # name is yours; must match gateway.reloadTopic + gw-reload path
65
+ mode: broadcast
66
+ exchange: rlb
67
+ routingKey: rlb-gateway-control
68
+ ```
69
+
70
+ Point `loadConfig.paths` at the export responder so DB routes merge with YAML
71
+ `gateway.paths` on boot and on every reload (merged static-before-param):
72
+
73
+ ```yaml
74
+ gateway:
75
+ loadConfig:
76
+ paths: { topic: rlb-gateway-admin, action: gw-path-export }
77
+ reloadTopic: rlb-gateway-control
78
+ metrics: { topic: rlb-gateway-admin, action: gw-metrics-track } # auto-emit track per request
79
+ ```
80
+
81
+ ## Route management — `gw-path-*` (id-keyed)
82
+
83
+ `gw-path-create` is a **POST** (DB paths have an `id`) — unlike the name-keyed
84
+ auth/ACL resources. `create` rejects a `(method, path)` collision (409).
85
+ `export` returns enabled paths, ordered, and feeds `loadConfig.paths`.
86
+
87
+ | Method | Path | action | dataSource | Notes |
88
+ | --- | --- | --- | --- | --- |
89
+ | POST | `/admin/paths` | `gw-path-create` | body | needs `name,method,path,topic`; 409 on collision |
90
+ | GET | `/admin/paths` | `gw-path-list` | query | `?page=&limit=` |
91
+ | GET | `/admin/paths/export` | `gw-path-export` | query | enabled, ordered; used by `loadConfig.paths` |
92
+ | PUT | `/admin/paths` | `gw-path-update` | body | needs `id`; re-checks collisions |
93
+ | GET | `/admin/paths/get` | `gw-path-get` | query | `?id=` |
94
+ | DELETE | `/admin/paths` | `gw-path-delete` | body | `{ id }` |
95
+
96
+ ## Auth-provider management — `gw-auth-*` (name-keyed PUT-upsert)
97
+
98
+ No `id`, no POST — a single `PUT` creates-or-updates by `name`. These are the
99
+ DB-stored providers, ON TOP of the static `auth-providers[]` in YAML.
100
+
101
+ | Method | Path | action | dataSource | Notes |
102
+ | --- | --- | --- | --- | --- |
103
+ | GET | `/admin/auth` | `gw-auth-list` | query | `?page=&limit=` |
104
+ | PUT | `/admin/auth` | `gw-auth-update` | body | upsert by name; `{ name, type, ... }` |
105
+ | GET | `/admin/auth/get` | `gw-auth-get` | query | `?name=` |
106
+ | DELETE | `/admin/auth` | `gw-auth-delete` | body | `{ name }` |
107
+
108
+ `gw-auth-export` (dump all enabled) also exists; not in the sample YAML.
109
+
110
+ ## Metrics — `gw-metrics-*` + `gw-health`
111
+
112
+ | Method | Path | action | mode | dataSource | Returns |
113
+ | --- | --- | --- | --- | --- | --- |
114
+ | GET | `/admin/metrics` | `gw-metrics-get` | rpc | query | counters/route (`count,errorCount,avgDurationMs`); `?route=` |
115
+ | GET | `/admin/metrics/series` | `gw-metrics-series` | rpc | query | buckets: `?bucketMs=60000&from=&to=&method=&route=&name=` |
116
+ | GET | `/admin/metrics/points` | `gw-metrics-points` | rpc | query | raw points newest-first: `?method=&route=&from=&to=&limit=` |
117
+ | POST | `/admin/metrics/track` | `gw-metrics-track` | event | body | fire-and-forget sink (wired via `gateway.metrics`) |
118
+
119
+ `gw-health` → `{ status: 'ok' }`, a real 200 liveness probe (NOT a metrics dump):
120
+
121
+ ```yaml
122
+ - name: health
123
+ method: GET
124
+ path: /health
125
+ dataSource: query
126
+ topic: rlb-gateway-admin
127
+ action: gw-health
128
+ mode: rpc
129
+ ```
130
+
131
+ ### In-proxy metrics hook — `RLB_GTW_METRICS_HOOK`
132
+
133
+ Independent of the broker `gateway.metrics` sink: the gateway invokes a hook
134
+ once per request, after the response is flushed. Register under
135
+ `RLB_GTW_METRICS_HOOK` in `ProxyModule.forRootAsync`'s `providers`. Both sinks
136
+ can be active; the hook must not throw and should be cheap/async.
137
+
138
+ ```ts
139
+ export interface GatewayMetricsHook { track(p: GatewayMetricPoint): void | Promise<void>; }
140
+ // GatewayMetricPoint: { ts, method, route, name?, topic?, action?, mode?, status?, durationMs? }
141
+
142
+ ProxyModule.forRootAsync({ /* ... */ providers: [
143
+ { provide: RLB_GTW_METRICS_HOOK, useClass: InfluxMetricsHook },
144
+ ]}),
145
+ ```
146
+
147
+ (`sample/config-sample/gateway-db` ships an `InfluxMetricsHook` that is a no-op
148
+ until `INFLUX_URL/TOKEN/ORG` env are set.)
149
+
150
+ ## Runtime reload — `gw-reload`
151
+
152
+ The ONLY control action that forces a route rebuild. Published to the
153
+ **broadcast control topic** (`gateway.reloadTopic`, NOT `rlb-gateway-admin`),
154
+ `mode: event`. The control-topic subscriber ignores everything except
155
+ `gw-reload`, staying decoupled from other control traffic.
156
+
157
+ ```yaml
158
+ - name: gw-reload
159
+ method: POST
160
+ path: /admin/reload
161
+ dataSource: body
162
+ topic: rlb-gateway-control # the broadcast control topic
163
+ action: gw-reload
164
+ mode: event
165
+ ```
166
+
167
+ Seed DB routes via `POST /admin/paths`, then `POST /admin/reload` — no restart.
168
+
169
+ ## Route auto-discovery (publisher vs consumer)
170
+
171
+ A microservice announces its own `@BrokerHTTP`/`@BrokerAction` routes; the
172
+ gateway persists + registers them, no YAML edits. Two halves must agree on the
173
+ same exchange/queue (defaults `rlb-route-discovery` / `rlb-route-sync`).
174
+
175
+ ### Publisher (microservice → gateway) — `broker.routeDiscovery`
176
+
177
+ Lives in `BrokerModule`, so its config is INSIDE the broker block.
178
+
179
+ | Field | Default | Purpose |
180
+ | --- | --- | --- |
181
+ | `serviceName` | — | **Required to publish**; ownership key. PROMOTES to AMQP `connection_name` if none set. |
182
+ | `publishOnBoot` | `true` | Announce manifest on bootstrap (durable/persistent message; queue buffers it). |
183
+ | `exchange` | `rlb-route-discovery` | Fanout exchange the manifest is published to. |
184
+ | `queue` | `rlb-route-sync` | Durable shared work-queue the gateway consumes. |
185
+
186
+ ```yaml
187
+ # in the MICROSERVICE config.yaml:
188
+ broker:
189
+ routeDiscovery:
190
+ serviceName: demo-ms # required; also fills connection_name if unset
191
+ publishOnBoot: true
192
+ # exchange/queue default; override only to namespace per env (then match the consumer)
193
+ ```
194
+
195
+ Routes are declared `@BrokerHTTP` over a `@BrokerAction` method. The gateway must
196
+ still declare the microservice's broker topic so it can route forwarded calls.
197
+
198
+ Each published route's auth comes from `@BrokerAuth` (decoupled from `@BrokerHTTP`),
199
+ paired per route by name: with one `@BrokerHTTP` it auto-pairs; with multiple, each
200
+ `@BrokerHTTP` sets a `name` and each `@BrokerAuth` matches it via `httpName`. So two
201
+ routes over the same action can publish with different auth — a route with no paired
202
+ `@BrokerAuth` is published as public.
203
+
204
+ ```ts
205
+ @BrokerHTTP('GET', '/admin/bookings/:id', 'params', { name: 'admin-get-booking' })
206
+ @BrokerAuth('admin-jwks', undefined, ['admin'], 'admin-get-booking') // pairs by httpName
207
+ ```
208
+
209
+ ### Consumer (gateway ← microservice) — `GatewayAdminModule` `routeDiscovery`
210
+
211
+ Wired by `GatewayAdminModule` (NOT YAML). Asserts the fanout exchange, subscribes
212
+ to the durable queue (competing consumers), then per manifest: diffs vs DB scoped
213
+ to the publishing service, applies only changes (insert/update; soft-disable stale
214
+ to `enabled:false`; skip collisions — existing owner keeps `(method,path)`),
215
+ journals every event via `RouteSyncLogRepository`, and publishes `gw-reload` when
216
+ anything changed. Never throws (acks; no poison loop).
217
+
218
+ ```ts
219
+ GatewayAdminModule.forRootAsync({
220
+ imports: [ConfigModule], inject: [ConfigService],
221
+ useFactory: (c: ConfigService) => ({
222
+ routeDiscovery: {
223
+ exchange: c.get('routeDiscovery.exchange'), // default rlb-route-discovery
224
+ queue: c.get('routeDiscovery.queue'), // default rlb-route-sync — MUST match publishers
225
+ },
226
+ }),
227
+ providers: [ /* HttpPathRepository, AuthProviderRepository, HttpMetricRepository, RouteSyncLogRepository */ ],
228
+ }),
229
+ ```
230
+
231
+ The consumer has NO `serviceName` (it only receives). The exchange/queue MUST
232
+ match every publisher's `broker.routeDiscovery`.
233
+
234
+ ## Verify
235
+
236
+ - Topic `rlb-gateway-admin` (+ queue) declared; `reloadTopic` matches the
237
+ broadcast control topic name and the `gw-reload` path's `topic`.
238
+ - Action strings written exactly (`gw-path-*`, `gw-auth-*`, `gw-metrics-*`,
239
+ `gw-health`, `gw-reload`).
240
+ - `loadConfig.paths` → `gw-path-export`; `gateway.metrics` → `gw-metrics-track`.
241
+ - Route-discovery `exchange`/`queue` identical on publisher and consumer.
242
+ - All four repositories bound; `npm run build`.
243
+
244
+ See also: `rlb-amqp` (schema/gotchas) · `rlb-amqp-add-route` · `docs/gateway.md` · `docs/acl.md`