@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.
- package/README.md +4 -2
- package/amqp-lib/config/rabbitmq.config.d.ts +6 -0
- package/modules/acl/const.d.ts +0 -1
- package/modules/acl/const.js +0 -1
- package/modules/acl/const.js.map +1 -1
- package/modules/acl/models.d.ts +5 -7
- package/modules/acl/repository/acl-action.repository.d.ts +1 -5
- package/modules/acl/repository/acl-action.repository.js.map +1 -1
- package/modules/acl/repository/acl-role.repository.d.ts +1 -5
- package/modules/acl/repository/acl-role.repository.js.map +1 -1
- package/modules/acl/services/acl-management.service.d.ts +2 -2
- package/modules/acl/services/acl-management.service.js +17 -20
- package/modules/acl/services/acl-management.service.js.map +1 -1
- package/modules/acl/services/acl.service.d.ts +1 -2
- package/modules/acl/services/acl.service.js +5 -21
- package/modules/acl/services/acl.service.js.map +1 -1
- package/modules/broker/broker.module.d.ts +2 -4
- package/modules/broker/broker.module.js +23 -5
- package/modules/broker/broker.module.js.map +1 -1
- package/modules/broker/config/decorator-paths.d.ts +1 -0
- package/modules/broker/config/decorator-paths.js +34 -4
- package/modules/broker/config/decorator-paths.js.map +1 -1
- package/modules/broker/config/route-discovery.config.d.ts +2 -0
- package/modules/broker/const.d.ts +1 -0
- package/modules/broker/const.js +2 -1
- package/modules/broker/const.js.map +1 -1
- package/modules/broker/decorators/broker-action.decorator.d.ts +2 -1
- package/modules/broker/decorators/broker-action.decorator.js +2 -2
- package/modules/broker/decorators/broker-action.decorator.js.map +1 -1
- package/modules/broker/services/broker.service.js +1 -1
- package/modules/broker/services/broker.service.js.map +1 -1
- package/modules/broker/services/metadata-scanner.service.js +11 -2
- package/modules/broker/services/metadata-scanner.service.js.map +1 -1
- package/modules/broker/services/route-discovery-publisher.service.js +7 -5
- package/modules/broker/services/route-discovery-publisher.service.js.map +1 -1
- package/modules/gateway-admin/config/gateway-admin.config.d.ts +4 -0
- package/modules/gateway-admin/const.d.ts +1 -1
- package/modules/gateway-admin/const.js +1 -1
- package/modules/gateway-admin/const.js.map +1 -1
- package/modules/gateway-admin/gateway-admin.module.d.ts +7 -1
- package/modules/gateway-admin/gateway-admin.module.js +13 -0
- package/modules/gateway-admin/gateway-admin.module.js.map +1 -1
- package/modules/gateway-admin/repository/auth-provider.repository.d.ts +3 -4
- package/modules/gateway-admin/repository/auth-provider.repository.js.map +1 -1
- package/modules/gateway-admin/services/gateway-auth.service.d.ts +3 -4
- package/modules/gateway-admin/services/gateway-auth.service.js +15 -23
- package/modules/gateway-admin/services/gateway-auth.service.js.map +1 -1
- package/modules/gateway-admin/services/gateway-metrics.service.d.ts +3 -0
- package/modules/gateway-admin/services/gateway-metrics.service.js +9 -0
- package/modules/gateway-admin/services/gateway-metrics.service.js.map +1 -1
- package/modules/gateway-admin/services/route-sync.service.d.ts +3 -1
- package/modules/gateway-admin/services/route-sync.service.js +14 -8
- package/modules/gateway-admin/services/route-sync.service.js.map +1 -1
- package/modules/proxy/services/http-handler.service.d.ts +3 -0
- package/modules/proxy/services/http-handler.service.js +27 -3
- package/modules/proxy/services/http-handler.service.js.map +1 -1
- package/package.json +5 -1
- package/schematics/nest-add/files/acl/src/cache/in-memory-acl-store.ts +54 -0
- package/schematics/nest-add/files/acl/src/modules/database/repository/acl.repository.ts +74 -0
- package/schematics/nest-add/files/db-core/src/modules/database/repository/in-memory-collection.ts +122 -0
- package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/gateway.repository.ts +151 -0
- package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/route-sync.repository.ts +15 -0
- package/schematics/nest-add/files/skills/rlb-amqp/SKILL.md +33 -5
- package/schematics/nest-add/files/skills/rlb-amqp/references/config-schema.md +182 -93
- package/schematics/nest-add/files/skills/rlb-amqp/references/gotchas.md +129 -79
- package/schematics/nest-add/files/skills/rlb-amqp-acl/SKILL.md +185 -0
- package/schematics/nest-add/files/skills/rlb-amqp-add-action/SKILL.md +87 -2
- package/schematics/nest-add/files/skills/rlb-amqp-add-route/SKILL.md +82 -19
- package/schematics/nest-add/files/skills/rlb-amqp-add-ws-event/SKILL.md +40 -18
- package/schematics/nest-add/files/skills/rlb-amqp-gateway-admin/SKILL.md +244 -0
- package/schematics/nest-add/files/skills/rlb-amqp-scaffold/SKILL.md +177 -42
- package/schematics/nest-add/index.js +612 -142
- package/schematics/nest-add/index.js.map +1 -1
- package/schematics/nest-add/index.ts +673 -241
- package/schematics/nest-add/init.schema.d.ts +10 -1
- package/schematics/nest-add/init.schema.ts +29 -3
- 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
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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` →
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`
|
|
50
|
-
|
|
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
|
|
60
|
-
- route-param vs body/query key collisions are intentional (
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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`
|