@open-rlb/nestjs-amqp 2.0.8 → 2.0.10

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 (110) hide show
  1. package/README.md +44 -45
  2. package/modules/acl/auth-match.d.ts +6 -0
  3. package/modules/acl/auth-match.js +10 -0
  4. package/modules/acl/auth-match.js.map +1 -0
  5. package/modules/acl/config/acl.config.d.ts +1 -0
  6. package/modules/acl/const.d.ts +5 -2
  7. package/modules/acl/const.js +6 -3
  8. package/modules/acl/const.js.map +1 -1
  9. package/modules/acl/index.d.ts +1 -0
  10. package/modules/acl/index.js +1 -0
  11. package/modules/acl/index.js.map +1 -1
  12. package/modules/acl/repository/acl-action.repository.d.ts +1 -0
  13. package/modules/acl/repository/acl-action.repository.js.map +1 -1
  14. package/modules/acl/repository/acl-grant.repository.d.ts +1 -0
  15. package/modules/acl/repository/acl-grant.repository.js.map +1 -1
  16. package/modules/acl/repository/acl-role.repository.d.ts +1 -0
  17. package/modules/acl/repository/acl-role.repository.js.map +1 -1
  18. package/modules/acl/services/acl-management.service.d.ts +10 -3
  19. package/modules/acl/services/acl-management.service.js +60 -10
  20. package/modules/acl/services/acl-management.service.js.map +1 -1
  21. package/modules/acl/services/acl.service.d.ts +3 -4
  22. package/modules/acl/services/acl.service.js +24 -46
  23. package/modules/acl/services/acl.service.js.map +1 -1
  24. package/modules/broker/config/decorator-paths.js +2 -2
  25. package/modules/broker/config/decorator-paths.js.map +1 -1
  26. package/modules/broker/decorators/broker-action.decorator.d.ts +1 -1
  27. package/modules/broker/decorators/broker-action.decorator.js +2 -2
  28. package/modules/broker/decorators/broker-action.decorator.js.map +1 -1
  29. package/modules/broker/services/shutdown-state.service.d.ts +1 -0
  30. package/modules/broker/services/shutdown-state.service.js +12 -12
  31. package/modules/broker/services/shutdown-state.service.js.map +1 -1
  32. package/modules/gateway-admin/config/gateway-admin.config.d.ts +2 -0
  33. package/modules/gateway-admin/const.d.ts +7 -0
  34. package/modules/gateway-admin/const.js +7 -0
  35. package/modules/gateway-admin/const.js.map +1 -1
  36. package/modules/gateway-admin/gateway-admin.module.js +4 -1
  37. package/modules/gateway-admin/gateway-admin.module.js.map +1 -1
  38. package/modules/gateway-admin/health.d.ts +14 -0
  39. package/modules/gateway-admin/health.js +5 -0
  40. package/modules/gateway-admin/health.js.map +1 -0
  41. package/modules/gateway-admin/index.d.ts +5 -0
  42. package/modules/gateway-admin/index.js +5 -0
  43. package/modules/gateway-admin/index.js.map +1 -1
  44. package/modules/gateway-admin/models.d.ts +61 -0
  45. package/modules/gateway-admin/repository/auth-provider.repository.d.ts +1 -0
  46. package/modules/gateway-admin/repository/auth-provider.repository.js.map +1 -1
  47. package/modules/gateway-admin/repository/http-metric.repository.d.ts +6 -2
  48. package/modules/gateway-admin/repository/http-metric.repository.js.map +1 -1
  49. package/modules/gateway-admin/repository/http-path.repository.d.ts +3 -0
  50. package/modules/gateway-admin/repository/http-path.repository.js.map +1 -1
  51. package/modules/gateway-admin/repository/route-sync-log.repository.d.ts +4 -1
  52. package/modules/gateway-admin/repository/route-sync-log.repository.js.map +1 -1
  53. package/modules/gateway-admin/services/gateway-auth.service.d.ts +1 -0
  54. package/modules/gateway-admin/services/gateway-auth.service.js +12 -0
  55. package/modules/gateway-admin/services/gateway-auth.service.js.map +1 -1
  56. package/modules/gateway-admin/services/gateway-health.service.d.ts +10 -0
  57. package/modules/gateway-admin/services/gateway-health.service.js +64 -0
  58. package/modules/gateway-admin/services/gateway-health.service.js.map +1 -0
  59. package/modules/gateway-admin/services/gateway-metrics-rollup.service.d.ts +13 -0
  60. package/modules/gateway-admin/services/gateway-metrics-rollup.service.js +63 -0
  61. package/modules/gateway-admin/services/gateway-metrics-rollup.service.js.map +1 -0
  62. package/modules/gateway-admin/services/gateway-metrics.service.d.ts +5 -4
  63. package/modules/gateway-admin/services/gateway-metrics.service.js +56 -17
  64. package/modules/gateway-admin/services/gateway-metrics.service.js.map +1 -1
  65. package/modules/gateway-admin/services/gateway-path.service.d.ts +11 -4
  66. package/modules/gateway-admin/services/gateway-path.service.js +79 -11
  67. package/modules/gateway-admin/services/gateway-path.service.js.map +1 -1
  68. package/modules/gateway-admin/services/gateway-retention.service.d.ts +16 -0
  69. package/modules/gateway-admin/services/gateway-retention.service.js +71 -0
  70. package/modules/gateway-admin/services/gateway-retention.service.js.map +1 -0
  71. package/modules/gateway-admin/services/route-sync.service.js +12 -5
  72. package/modules/gateway-admin/services/route-sync.service.js.map +1 -1
  73. package/modules/gateway-admin/util/metrics.d.ts +10 -0
  74. package/modules/gateway-admin/util/metrics.js +170 -0
  75. package/modules/gateway-admin/util/metrics.js.map +1 -0
  76. package/modules/gateway-admin/util/route-diff.d.ts +7 -0
  77. package/modules/gateway-admin/util/route-diff.js +7 -2
  78. package/modules/gateway-admin/util/route-diff.js.map +1 -1
  79. package/modules/gateway-admin/util/route-manifest.d.ts +3 -0
  80. package/modules/gateway-admin/util/route-manifest.js +36 -1
  81. package/modules/gateway-admin/util/route-manifest.js.map +1 -1
  82. package/modules/proxy/config/path-definition.config.d.ts +2 -2
  83. package/modules/proxy/services/acl.service.d.ts +2 -2
  84. package/modules/proxy/services/acl.service.js.map +1 -1
  85. package/modules/proxy/services/http-auth-handler.service.d.ts +6 -4
  86. package/modules/proxy/services/http-auth-handler.service.js +15 -9
  87. package/modules/proxy/services/http-auth-handler.service.js.map +1 -1
  88. package/modules/proxy/services/http-handler.service.d.ts +1 -0
  89. package/modules/proxy/services/http-handler.service.js +28 -35
  90. package/modules/proxy/services/http-handler.service.js.map +1 -1
  91. package/modules/proxy/services/metrics-hook.d.ts +1 -0
  92. package/modules/proxy/services/websocket.service.js +5 -5
  93. package/modules/proxy/services/websocket.service.js.map +1 -1
  94. package/package.json +1 -1
  95. package/schematics/nest-add/files/acl/src/modules/database/repository/acl.repository.ts +3 -0
  96. package/schematics/nest-add/files/db-core/src/modules/database/repository/in-memory-collection.ts +29 -0
  97. package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/gateway.repository.ts +48 -20
  98. package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/route-sync.repository.ts +24 -1
  99. package/schematics/nest-add/files/skills/rlb-amqp/SKILL.md +3 -3
  100. package/schematics/nest-add/files/skills/rlb-amqp/references/config-schema.md +18 -14
  101. package/schematics/nest-add/files/skills/rlb-amqp/references/gotchas.md +42 -17
  102. package/schematics/nest-add/files/skills/rlb-amqp-acl/SKILL.md +57 -39
  103. package/schematics/nest-add/files/skills/rlb-amqp-add-action/SKILL.md +4 -3
  104. package/schematics/nest-add/files/skills/rlb-amqp-add-route/SKILL.md +20 -14
  105. package/schematics/nest-add/files/skills/rlb-amqp-add-ws-event/SKILL.md +7 -6
  106. package/schematics/nest-add/files/skills/rlb-amqp-gateway-admin/SKILL.md +49 -9
  107. package/schematics/nest-add/files/skills/rlb-amqp-scaffold/SKILL.md +4 -4
  108. package/schematics/nest-add/index.js +78 -9
  109. package/schematics/nest-add/index.js.map +1 -1
  110. package/schematics/nest-add/index.ts +78 -9
@@ -134,7 +134,7 @@ auth-providers:
134
134
  - name: gateway-jwks
135
135
  type: jwks # jwt | jwks | basic | str-compare | none
136
136
  headerPrefix: "X-GTW-AUTH-" # prefix for mapped claim headers (and <prefix>USERID)
137
- uidClaim: sub # claim → <prefix>USERID; REQUIRED for role checks
137
+ uidClaim: sub # claim → <prefix>USERID; REQUIRED for action checks
138
138
  jwtMap: # 'source:dest' pairs → <prefix><DEST>; WITHOUT it NO claims forwarded
139
139
  - sub:userId # → X-GTW-AUTH-USERID
140
140
  - email:email # → X-GTW-AUTH-EMAIL
@@ -159,8 +159,8 @@ Type behaviour:
159
159
 
160
160
  Hardening: `algorithms` REQUIRED for `jwt`/`jwks` (omit → denied; algorithm-confusion guard).
161
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`).
162
+ `usernameClaim` is deprecated; `aclTopic`/`aclAction` are removed (the gateway action check is
163
+ in-process via `IAclRoleService.checkAction(userId, ctx, action)`).
164
164
 
165
165
  > DB-stored auth-providers (name-keyed `gw-auth-*` upserts) layer on top of this static list —
166
166
  > see `docs/gateway-admin.md`.
@@ -211,8 +211,8 @@ control-topic subscriber ignores every other message. Concurrent `reload()`s are
211
211
  mode: rpc # rpc | event
212
212
  timeout: 15000 # rpc only (ms)
213
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
214
+ allowAnonymous: false # true → skip the auth/action gate entirely
215
+ actions: [doc.read, doc.admin] # caller must hold AT LEAST ONE on (companyId, resourceId); needs auth + IAclRoleService
216
216
  successStatusCode: 200 # default 200 rpc / 202 event / 204 empty rpc reply
217
217
  binary: true # treat a raw (non-JSON) reply as base64 → binary body
218
218
  redirect: 302 # rpc only → redirect with this status, using the reply as Location
@@ -238,9 +238,13 @@ Uploads → `$files` (buffers as binary strings), raw → `$raw`.
238
238
  ForbiddenError→403, NotFoundError→404, ConflictError→409, else→500.
239
239
  **event status:** successful publish → `successStatusCode || 202`; publish failure → `503`.
240
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).
241
+ Auth gate (per request): `allowAnonymous:true` → gate skipped; `auth` no `actions` → authn only
242
+ (401 if invalid); `auth` + `actions` → authn then in-process
243
+ `checkAction(userId, { companyId, resourceId }, actions)` (403 if the caller holds none of
244
+ `actions` on that pair). `actions` without `auth` fails closed (every request 403).
245
+
246
+ The gateway reads the canonical `companyId`/`resourceId` from the request (precedence
247
+ params → query → body) and matches them exactly for the action check.
244
248
 
245
249
  ### gateway.events[] (WebSocketEvent — WS / webhook)
246
250
 
@@ -251,7 +255,7 @@ no role). `roles` without `auth` fails closed (every request 403).
251
255
  routingKey: chat.messages
252
256
  auth: gateway-jwks # provider that verifies the token + maps claims FOR THIS event (at subscribe)
253
257
  requireAuth: true # default true when `auth` is set; false → auth optional (anon allowed)
254
- roles: [user] # ACL check via IAclRoleService (needs auth)
258
+ actions: [chat.read] # ACL check via IAclRoleService.checkAction (needs auth); WS gates resource-agnostically
255
259
  scopeClaim: userId # per-user isolation: the mapped claim value...
256
260
  payloadKey: userId # ...must equal payload[payloadKey]; without payloadKey → denies everything
257
261
  # type: http only:
@@ -282,11 +286,10 @@ ACL (topic `rlb-acl`):
282
286
 
283
287
  | Method | Path | Action |
284
288
  | --- | --- | --- |
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) |
289
+ | GET | `/acl/check` | `acl-check-action` (`?userId=&action=&companyId?=&resourceId?=`; exact-match; `200` true/false) |
287
290
  | 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) |
291
+ | POST | `/acl/grants` | `acl-grant` (`{userId, roles, resourceId?, companyId?}`; caller needs `role-management` on target) |
292
+ | DELETE | `/acl/grants` | `acl-revoke` (same shape; `roles` REQUIRED; caller needs `role-management` on target) |
290
293
  | GET / PUT / DELETE | `/acl/actions[/get?name=]` | `acl-action-list`/`-update`/`-delete`/`-get` |
291
294
  | GET / PUT / DELETE | `/acl/roles[/get?name=]` | `acl-role-list`/`-update`/`-delete`/`-get` |
292
295
 
@@ -301,4 +304,5 @@ Gateway-admin (topic `rlb-gateway-admin`, except reload):
301
304
  | POST | `/admin/metrics/track` | `gw-metrics-track` (`mode: event`) |
302
305
  | POST | `/admin/reload` | `gw-reload` on `rlb-gateway-control` (`mode: event`) |
303
306
 
304
- > Removed: `acl-list-by-user`, `acl-verify-access`, `gw-auth-create`, all id-based ACL CRUD.
307
+ > Removed: `acl-list-by-user`, `acl-verify-access`, `acl-can-user-do`, `acl-can-user-do-gtw`
308
+ > (all collapsed into `acl-check-action`), `gw-auth-create`, all id-based ACL CRUD.
@@ -76,29 +76,54 @@ Ported from `docs/gotchas.md` (re-verified against post-2.0.5 code).
76
76
  NOT a metrics dump. Use `/admin/metrics*` (`gw-metrics-*`) for metrics.
77
77
 
78
78
  ## Auth / ACL
79
- 21. **`roles` require `auth` on the same path/event.** No `auth` → no identity → fails closed
80
- (every request `403`, logged at boot). Always pair `roles: [...]` with `auth: <provider>`.
81
- 22. **`roles` require an `IAclRoleService`** registered via `RLB_GTW_ACL_ROLE_SERVICE` in
79
+ 21. **`actions` require `auth` on the same path/event.** No `auth` → no identity → fails closed
80
+ (every request `403`, logged at boot). Always pair `actions: [...]` with `auth: <provider>`.
81
+ 22. **`actions` require an `IAclRoleService`** registered via `RLB_GTW_ACL_ROLE_SERVICE` in
82
82
  `ProxyModule.forRootAsync({ providers: [...] })`. Missing → deny (403). The gateway check is
83
- **role-based, OR, resource-agnostic** (`canUserDoGtw(roles, userId)`): `roles` lists ROLE NAMES,
84
- the user passes holding AT LEAST ONE. The provider only needs `uidClaim` (+ `headerPrefix`).
85
- 23. **Two ACL check actions on `rlb-acl`** (both cached, both HTTP GET → `200` true/false):
86
- `acl-can-user-do-gtw` → `canUserDoGtw(roles, userId)` (gateway filter, OR, resource-agnostic,
87
- `GET /acl/check`) and `acl-can-user-do` `canUserDo(roles, userId, resource)` (**ms-side**;
88
- a global grant OR a grant on that resource passes, `GET /acl/check-resource`).
83
+ **action-based, OR, resource-SCOPED** (`checkAction(userId, ctx, actions)`): `actions` lists
84
+ ACTION NAMES, the caller is authorized if it holds AT LEAST ONE on the request's
85
+ `(companyId, resourceId)`. The provider only needs `uidClaim` (+ `headerPrefix`).
86
+ 23. **One ACL check action on `rlb-acl`: `acl-check-action`** (cached, HTTP GET `200` true/false).
87
+ `checkAction(userId, ctx, action)`, `ctx = { companyId?, resourceId? }`,
88
+ `action = string | string[]` (OR). It resolves action→roles-that-include-it, then matches the
89
+ user's grants. A grant authorizes **iff** `grant.companyId === req.companyId &&
90
+ grant.resourceId === req.resourceId` (undefined/null/`''` = absent). The ONLY carve-out: both
91
+ ids absent on request AND grant. **No wildcard** — a `null` `resourceId` does NOT match
92
+ everything; `companyId` is load-bearing. Replaces the old `acl-can-user-do` /
93
+ `acl-can-user-do-gtw` and the merged `GET /acl/check` + `/acl/check-resource`.
94
+ 23a. **Gateway gating is ACTION-based, not role-based.** `gateway.paths[].actions` /
95
+ `events[].actions` name ACTIONS (was `roles`). The gateway resolves `userId` from the auth
96
+ provider, extracts `(companyId, resourceId)` from the request, and authorizes if the caller
97
+ holds one of `actions` on that pair. It reads the canonical `companyId`/`resourceId` from the
98
+ request (precedence params→query→body) and matches them exactly. WS events gate by `actions`
99
+ **resource-agnostically**.
100
+ 23b. **`@BrokerAuth`'s 3rd param is now `actions` (was `roles`).** Signature:
101
+ `@BrokerAuth(authName, allowAnonymous?, actions?, httpName?)`. Pass action names there for an
102
+ auto-discovered route's action gate.
89
103
  24. **Actions, roles & auth-providers are NAME-KEYED. PUT upserts; there is NO POST.** The `name`
90
104
  IS the key (no id). `PUT` creates-or-updates, `GET` lists, `GET .../get?name=` reads one,
91
105
  `DELETE` removes by `name`. The old id-based ACL CRUD and `POST`-create endpoints are GONE.
92
106
  (Gateway-admin **paths** are the exception — they keep id-keyed CRUD and a POST create.)
93
107
  25. **`acl-grant` / `acl-revoke` both REQUIRE `userId` + `roles`** (optional `resourceId` +
94
- `companyId`). `grant` MERGES roles into the single `(userId, resourceId)` record (idempotent).
95
- `revoke` REMOVES exactly those roles and **deletes the record once it has no roles left**.
96
- `revoke` without `roles` throws `400 roles are required` — to wipe a grant, revoke all its roles.
97
- 26. **`companyId` is grouping metadata only.** It replaced `resourceBusinessId` and plays NO part
98
- in authorization it only groups resources in `acl-list-resources-by-user`. Targeting is by
99
- `(userId, resourceId)` only. Both grant/revoke validate every role exists (unknown `400`).
100
- 27. **Removed actions:** `acl-list-by-user` and `acl-verify-access` no longer exist. Use
101
- `acl-can-user-do` for resource-scoped checks and `acl-list-resources-by-user` to list resources.
108
+ `companyId`). The grant record is keyed by `(userId, companyId, resourceId)`. `grant` MERGES
109
+ roles into that triple (idempotent). `revoke` REMOVES exactly those roles and **deletes the
110
+ record once it has no roles left**. `revoke` without `roles` throws `400 roles are required` —
111
+ to wipe a grant, revoke all its roles. **Grants assign ROLES (keep the `roles` param); roles
112
+ contain actions.** Only the gateway/route GATE switched to action names.
113
+ 25a. **`acl-grant` / `acl-revoke` are GATED.** The caller (forwarded `X-GTW-AUTH-USERID`) must hold
114
+ the `role-management` action on the TARGET `(companyId, resourceId)`, else `403`. The gate
115
+ action defaults to `role-management`, overridable via `AclModuleOptions.roleManagementAction`.
116
+ **Chicken-and-egg:** no caller can grant the very first `role-management`, so **bootstrap by
117
+ seeding the first `role-management` grant directly in the DB**.
118
+ 26. **`companyId` is LOAD-BEARING in authorization.** It replaced `resourceBusinessId` and is
119
+ BOTH part of the grant identity AND matched during `checkAction`: a grant authorizes only when
120
+ `grant.companyId === req.companyId` (and `resourceId` likewise). It also groups
121
+ `acl-list-resources-by-user` output. There is **no wildcard** — a `null`/absent `resourceId`
122
+ only matches a request with that id also absent. Both grant/revoke validate every role exists
123
+ (unknown → `400`).
124
+ 27. **Removed actions:** `acl-list-by-user`, `acl-verify-access`, `acl-can-user-do`, and
125
+ `acl-can-user-do-gtw` no longer exist (the last two collapsed into `acl-check-action`). Use
126
+ `acl-check-action` for authorization checks and `acl-list-resources-by-user` to list resources.
102
127
  28. **Auth & gateway config go to `ProxyModule`** (`authOptions` / `gatewayOptions`), not
103
128
  `BrokerModule`. `BrokerModule` owns only `options` / `topics` / `appOptions`.
104
129
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: rlb-amqp-acl
3
- description: Manage role-based access control (ACL) with @open-rlb/nestjs-amqp — actions, roles, grants/revokes, and "can user do X" checks. Use when wiring AclModule, gating gateway routes by roles, granting/revoking a user's roles, listing a user's resources, or answering authorization/permission questions (roles, grants, acl-check).
3
+ description: Manage access control (ACL) with @open-rlb/nestjs-amqp — actions, roles, grants/revokes, and "can user do X" checks. Use when wiring AclModule, gating gateway routes by actions, granting/revoking a user's roles, listing a user's resources, or answering authorization/permission questions (actions, roles, grants, acl-check).
4
4
  ---
5
5
 
6
6
  # Manage ACL (@open-rlb/nestjs-amqp)
@@ -11,15 +11,16 @@ Read first when you need depth:
11
11
  - `sample/config-sample/acl.yaml` (annotated broker + gateway reference)
12
12
  - `sample/config-sample/gateway-in-memory/src/app.module.ts` (forRoot wiring)
13
13
 
14
- Use when: managing **actions/roles/grants**, wiring `AclModule`, role-gating routes
15
- (`roles: [...]`), or answering "can user do X".
14
+ Use when: managing **actions/roles/grants**, wiring `AclModule`, action-gating routes
15
+ (`actions: [...]`), or answering "can user do X".
16
16
 
17
17
  ## Model (3 entities)
18
18
 
19
19
  - **Action** — atomic capability (`read-doc`). Name-keyed.
20
20
  - **Role** — bundle of action names (`editor = [read-doc, write-doc]`). Name-keyed.
21
- - **Grant** — binds a `userId` → role names; one record per `(userId, resourceId)`.
22
- - **Checks** match on **roles, never action strings**.
21
+ - **Grant** — binds a `userId` → role names; one record per `(userId, companyId, resourceId)`.
22
+ - **Checks** resolve the requested **action** → roles-that-include-it, then match the
23
+ user's grants. The route/gate names **actions**; grants still assign **roles**.
23
24
 
24
25
  ## Decorator-bound (NOT configurable)
25
26
 
@@ -28,8 +29,8 @@ reference them literally. The queue / exchange / routingKey that carry the topic
28
29
 
29
30
  `ACL_ACTIONS`: `acl-action-list`, `acl-action-get`, `acl-action-update`,
30
31
  `acl-action-delete`, `acl-role-list`, `acl-role-get`, `acl-role-update`,
31
- `acl-role-delete`, `acl-grant`, `acl-revoke`, `acl-can-user-do-gtw`,
32
- `acl-can-user-do`, `acl-list-resources-by-user`, `acl-invalidate`.
32
+ `acl-role-delete`, `acl-grant`, `acl-revoke`, `acl-check-action`,
33
+ `acl-list-resources-by-user`, `acl-invalidate`.
33
34
 
34
35
  > **Removed in 2.0.5:** `acl-list-by-user`, `acl-verify-access`, `acl-create` /
35
36
  > id-based ACL CRUD. Entities are name-keyed: **PUT upserts, no POST.**
@@ -38,30 +39,40 @@ reference them literally. The queue / exchange / routingKey that carry the topic
38
39
 
39
40
  No id, no POST. `PUT` upserts by `name` (idempotent), `GET` lists (`?page=&limit=`),
40
41
  `GET …/get?name=` reads one, `DELETE` removes by `name`. Role upsert: every referenced
41
- action must already exist (else **400**).
42
+ action must already exist (else **400**). The `*-search` actions (`acl-action-search`,
43
+ `acl-role-search`, `acl-grant-search`) now take `?q=&page=&limit=` and return
44
+ `PaginationModel<T>` (`{ page, limit, total, data }`), not a bare array.
42
45
 
43
- ## Grants — dual grant/revoke
46
+ ## Grants — dual grant/revoke (now GATED)
44
47
 
45
- One record per `(userId, resourceId)`. Both ops **require `userId` + `roles`**;
46
- `resourceId` + `companyId` are **optional**.
48
+ One record per `(userId, companyId, resourceId)`. Both ops **require `userId` + `roles`**;
49
+ `resourceId` + `companyId` are **optional** but PART of the record identity.
47
50
 
48
- - `acl-grant` — merges roles into the pair (creates if absent; idempotent).
51
+ - `acl-grant` — merges roles into the triple (creates if absent; idempotent).
49
52
  - `acl-revoke` — removes roles; deletes the record once empty.
50
53
  - Both validate every role exists (unknown role → **400**) and invalidate the user's cache.
51
- - `companyId` (replaced `resourceBusinessId`) is **grouping metadata only** it groups
52
- `acl-list-resources-by-user` output and plays **no part** in authorization.
53
-
54
- ## Checks GET 200 with `true`/`false`
55
-
56
- `false` is real content; only `null`/`undefined` collapses to 204. Both return `false`
57
- (never throw) on missing input or error.
58
-
59
- - `acl-can-user-do-gtw` — resource-**agnostic**, the gateway's primary filter. `true` if
60
- the user holds **≥1** requested role. Query: `?userId=&roles=user&roles=admin`.
61
- - `acl-can-user-do` — resource-**scoped**: `true` if a **global** grant OR a grant bound
62
- to that exact `resource` gives a matching role. Query: `?userId=&roles=admin&resource=doc-1`.
63
- Normally called over the broker by the owning microservice.
64
- - `acl-list-resources-by-user` — **auth-gated** (needs `auth`, no roles): reads `userId`
54
+ - `companyId` (replaced `resourceBusinessId`) is **load-bearing**: it is part of the grant
55
+ identity AND part of authorization (a grant matches only when its `companyId` equals the
56
+ request's). It also groups `acl-list-resources-by-user` output.
57
+ - **Caller gating:** `acl-grant`/`acl-revoke` require the caller (forwarded
58
+ `X-GTW-AUTH-USERID`) to hold the `role-management` action on the TARGET
59
+ `(companyId, resourceId)`, else **403**. The gate action defaults to `role-management`,
60
+ overridable via `AclModuleOptions.roleManagementAction`. Bootstrap by seeding the first
61
+ `role-management` grant directly in the DB (no caller can grant it otherwise).
62
+
63
+ ## Checks single primitive, GET 200 with `true`/`false`
64
+
65
+ `false` is real content; only `null`/`undefined` collapses to 204. Returns `false`
66
+ (never throws) on missing input or error.
67
+
68
+ - `acl-check-action` → `checkAction(userId, ctx, action)`, `ctx = { companyId?, resourceId? }`,
69
+ `action = string | string[]` (OR). Resolves the action(s) → roles-that-include-it, then
70
+ matches the user's grants. A grant authorizes **iff** `grant.companyId === req.companyId &&
71
+ grant.resourceId === req.resourceId` (undefined/null/`''` all count as absent). The ONLY
72
+ carve-out: both ids absent on the request AND on the grant. **No wildcard** — a `null`
73
+ `resourceId` no longer matches everything; `companyId` is load-bearing.
74
+ Query: `?userId=&action=read-doc&companyId=acme&resourceId=doc-1`.
75
+ - `acl-list-resources-by-user` — **auth-gated** (needs `auth`, no actions): reads `userId`
65
76
  from the forwarded `X-GTW-AUTH-USERID` header; lists accessible resources grouped by
66
77
  `companyId` with resolved actions.
67
78
 
@@ -88,8 +99,9 @@ AclModule.forRoot(
88
99
  );
89
100
  ```
90
101
 
91
- Gateway side — let route `roles: [...]` filters run **in-process** (no broker hop) by
92
- binding the gateway token to the same `AclService`:
102
+ Gateway side — let route `actions: [...]` gates run **in-process** (no broker hop) by
103
+ binding the gateway token to the same `AclService` (implements
104
+ `IAclRoleService.checkAction(userId, ctx, action)`):
93
105
 
94
106
  ```ts
95
107
  import { ProxyModule, AclService, RLB_GTW_ACL_ROLE_SERVICE } from '@open-rlb/nestjs-amqp';
@@ -99,8 +111,9 @@ ProxyModule.forRoot({
99
111
  });
100
112
  ```
101
113
 
102
- Same process → `useExisting`. Separate services → gateway RPCs `acl-can-user-do-gtw` on
103
- `rlb-acl` instead. A route's `roles` are ROLE NAMES; the user passes with **≥1**.
114
+ Same process → `useExisting`. Separate services → gateway RPCs `acl-check-action` on
115
+ `rlb-acl` instead. A route's `actions` are ACTION NAMES; the caller is authorized if it
116
+ holds **≥1** of them on the request's `(companyId, resourceId)`.
104
117
 
105
118
  ## YAML — topic + queue (names fixed, transport yours)
106
119
 
@@ -131,14 +144,16 @@ the fixed library string.
131
144
  | acl-action-get | GET | /acl/actions/get | query | acl-action-get |
132
145
  | acl-action-upsert | PUT | /acl/actions | body | acl-action-update |
133
146
  | acl-action-delete | DELETE | /acl/actions | body | acl-action-delete |
147
+ | acl-action-search | GET | /acl/actions/search | query | acl-action-search (`?q=&page=&limit=` → `PaginationModel<AclAction>`) |
134
148
  | acl-role-list | GET | /acl/roles | query | acl-role-list |
135
149
  | acl-role-get | GET | /acl/roles/get | query | acl-role-get |
136
150
  | acl-role-upsert | PUT | /acl/roles | body | acl-role-update |
137
151
  | acl-role-delete | DELETE | /acl/roles | body | acl-role-delete |
138
- | acl-grant | POST | /acl/grants | body | acl-grant |
139
- | acl-revoke | DELETE | /acl/grants | body | acl-revoke |
140
- | acl-check-gtw | GET | /acl/check | query | acl-can-user-do-gtw |
141
- | acl-check-resource | GET | /acl/check-resource | query | acl-can-user-do |
152
+ | acl-role-search | GET | /acl/roles/search | query | acl-role-search (`?q=&page=&limit=` → `PaginationModel<AclRole>`) |
153
+ | acl-grant | POST | /acl/grants | body | acl-grant (gated: caller needs `role-management`) |
154
+ | acl-revoke | DELETE | /acl/grants | body | acl-revoke (gated: caller needs `role-management`) |
155
+ | acl-grant-search | GET | /acl/grants/search | query | acl-grant-search — access search (`?q=&page=&limit=` → `PaginationModel<AclGrant>`) |
156
+ | acl-check | GET | /acl/check | query | acl-check-action |
142
157
  | acl-list-resources-by-user | GET | /acl/resources | query | acl-list-resources-by-user (+ `auth:`) |
143
158
 
144
159
  ```yaml
@@ -153,18 +168,18 @@ gateway:
153
168
  action: acl-role-update
154
169
  mode: rpc
155
170
  - name: acl-grant # body: { userId, roles, resourceId?, companyId?, friendlyName? }
156
- method: POST
171
+ method: POST # gated: caller (X-GTW-AUTH-USERID) needs role-management on target
157
172
  path: /acl/grants
158
173
  dataSource: body
159
174
  topic: rlb-acl
160
175
  action: acl-grant
161
176
  mode: rpc
162
- - name: acl-check-gtw # ?userId=&roles=user&roles=admin → 200 true/false
177
+ - name: acl-check # ?userId=&action=read-doc&companyId=&resourceId= → 200 true/false
163
178
  method: GET
164
179
  path: /acl/check
165
180
  dataSource: query
166
181
  topic: rlb-acl
167
- action: acl-can-user-do-gtw
182
+ action: acl-check-action
168
183
  mode: rpc
169
184
  - name: acl-list-resources-by-user # auth-gated; userId from X-GTW-AUTH-USERID
170
185
  method: GET
@@ -180,6 +195,9 @@ gateway:
180
195
 
181
196
  - topic `rlb-acl` + its queue declared on the consuming service; gateway paths use the
182
197
  literal `action` strings above.
183
- - role-gated routes (`roles: [...]`) → `RLB_GTW_ACL_ROLE_SERVICE` bound to an
184
- `IAclRoleService` (`AclService`). Auth-provider needs `uidClaim` (+ `headerPrefix`).
198
+ - action-gated routes (`actions: [...]`) → `RLB_GTW_ACL_ROLE_SERVICE` bound to an
199
+ `IAclRoleService` (`AclService`, `checkAction`). Auth-provider needs `uidClaim`
200
+ (+ `headerPrefix`).
201
+ - `acl-grant`/`acl-revoke` are gated — seed the first `role-management` grant directly in
202
+ the DB or every caller gets `403`.
185
203
  - a check returning `false` is a **200**, not an error.
@@ -97,8 +97,9 @@ Two independent pairings sit on the method, each only needed in the multi case:
97
97
  `@BrokerAuth` is **public**.
98
98
 
99
99
  Auth lives in a separate, decoupled decorator —
100
- `@BrokerAuth(authName, allowAnonymous?, roles?, httpName?)` — never inside `@BrokerHTTP`'s options.
101
- This lets two HTTP paths for the SAME action carry DIFFERENT auth.
100
+ `@BrokerAuth(authName, allowAnonymous?, actions?, httpName?)` — never inside `@BrokerHTTP`'s options.
101
+ The 3rd param is `actions` (ACL action names, was `roles`). This lets two HTTP paths for the SAME
102
+ action carry DIFFERENT auth.
102
103
 
103
104
  Simple case — one route, auth auto-pairs (no names needed):
104
105
 
@@ -118,7 +119,7 @@ Multi case — two routes for ONE action, each name-paired to its own auth:
118
119
  @BrokerHTTP('GET', '/bookings/:id', 'params', { name: 'get-booking' })
119
120
  @BrokerAuth('cust-jwks', true, undefined, 'get-booking') // httpName ⇄ route name
120
121
  @BrokerHTTP('GET', '/admin/bookings/:id', 'params', { name: 'admin-get-booking' })
121
- @BrokerAuth('admin-jwks', undefined, ['admin'], 'admin-get-booking')
122
+ @BrokerAuth('admin-jwks', undefined, ['booking.admin'], 'admin-get-booking') // 3rd param = ACL actions
122
123
  async getBooking(@BrokerParam('params', 'id') id: string) {
123
124
  return this.bookings.find(id);
124
125
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: rlb-amqp-add-route
3
- description: Expose a broker action over HTTP through the @open-rlb/nestjs-amqp gateway by adding a gateway.paths[] entry. Use when the user wants a new HTTP endpoint/REST route that forwards to a topic/action, choosing rpc (wait reply) vs event (fire-and-forget with confirm), with auth, roles, dataSource, timeout, file upload or raw body. Generates the YAML path fragment and flags required bootstrap/ACL wiring.
3
+ description: Expose a broker action over HTTP through the @open-rlb/nestjs-amqp gateway by adding a gateway.paths[] entry. Use when the user wants a new HTTP endpoint/REST route that forwards to a topic/action, choosing rpc (wait reply) vs event (fire-and-forget with confirm), with auth, actions (ACL gate), dataSource, timeout, file upload or raw body. Generates the YAML path fragment and flags required bootstrap/ACL wiring.
4
4
  ---
5
5
 
6
6
  # Add an HTTP gateway route (gateway.paths[])
@@ -20,7 +20,9 @@ Canonical example: `sample/config-sample/gateway-in-memory/config/config.yaml`.
20
20
  - **dataSource**: how the payload is assembled — `req.params` are ALWAYS merged in, plus:
21
21
  `body` | `query` | `params` | `body-query` (body wins) | `query-body` (query wins).
22
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
+ headers). `allowAnonymous: true` skips the gate. `actions: [...]` adds an ACL action check
24
+ scoped to the request's `(companyId, resourceId)` (read from the canonical fields,
25
+ params → query → body).
24
26
  - Extras: `timeout` (rpc), `successStatusCode`, `binary`, `redirect`, `parseRaw`, static
25
27
  `headers`, `forwardHeaders`.
26
28
 
@@ -36,7 +38,7 @@ Canonical example: `sample/config-sample/gateway-in-memory/config/config.yaml`.
36
38
  | `dataSource` | `body` \| `query` \| `params` \| `body-query` \| `query-body`. |
37
39
  | `auth` | Auth-provider name; validates + maps claims. |
38
40
  | `allowAnonymous` | `true` → gate skipped (token still mapped if present & valid). |
39
- | `roles` | Role NAMES; caller passes with AT LEAST ONE. Requires `auth`. |
41
+ | `actions` | ACTION NAMES; caller passes holding AT LEAST ONE on the request's `(companyId, resourceId)`. Requires `auth`. |
40
42
  | `timeout` | RPC timeout (ms), `rpc` only. |
41
43
  | `binary` | Treat a raw (non-JSON) RPC reply as base64 → binary body. |
42
44
  | `parseRaw` | Adds the raw request body as `$raw` (needs `rawBody: true`). |
@@ -60,7 +62,7 @@ gateway:
60
62
  action: <action>
61
63
  mode: rpc # or event
62
64
  auth: gateway-jwks # optional
63
- roles: [resource.write] # optional → needs RLB_GTW_ACL_ROLE_SERVICE
65
+ actions: [resource.write] # optional → needs RLB_GTW_ACL_ROLE_SERVICE; checked on (companyId, resourceId)
64
66
  timeout: 7000 # rpc only
65
67
  successStatusCode: 201
66
68
  ```
@@ -71,15 +73,18 @@ For every request the gateway runs `processAuthData` (best-effort), then:
71
73
 
72
74
  1. **`allowAnonymous: true`** → gate SKIPPED. A valid token still gets its claims mapped &
73
75
  forwarded; a missing/invalid token is NOT blocked.
74
- 2. **`auth` set, no `roles`** → authentication only. Provider must validate (else `401`);
76
+ 2. **`auth` set, no `actions`** → authentication only. Provider must validate (else `401`);
75
77
  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`.
78
+ 3. **`auth` + `actions`** → authn + action auth. After a valid token the gateway reads the
79
+ user id from the provider's `uidClaim`, extracts `(companyId, resourceId)` from the request
80
+ (canonical fields, params query body), and calls
81
+ `IAclRoleService.checkAction(userId, { companyId, resourceId }, actions)` in-process. Passes
82
+ if the caller holds at least one of `actions` on that pair, else `403`. The check is
83
+ **exact-match on `(companyId, resourceId)` — there is no wildcard**, and `companyId` is
84
+ load-bearing.
79
85
 
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.
86
+ > `actions` WITHOUT `auth` is a misconfiguration: no identity → fails closed (every request
87
+ > `403`, logged loudly at boot).
83
88
 
84
89
  ## Status mapping
85
90
 
@@ -109,9 +114,10 @@ For every request the gateway runs `processAuthData` (best-effort), then:
109
114
  ## Required wiring to flag
110
115
 
111
116
  - 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`) +
117
+ - If `actions` is used → an `IAclRoleService` (`checkAction`) must be registered via
118
+ `RLB_GTW_ACL_ROLE_SERVICE` in
119
+ `ProxyModule.forRootAsync({ providers: [{ provide: RLB_GTW_ACL_ROLE_SERVICE, useExisting: AclService }] })`.
120
+ If a path declares `actions` and the service is NOT registered → request DENIED (`403`) +
115
121
  error logged. The auth-provider needs a `uidClaim` (+ `headerPrefix`) to resolve the userId.
116
122
  - Forwarded auth claims reach the handler as prefixed/uppercased headers
117
123
  (e.g. `X-GTW-AUTH-USERID`) — read them with `@BrokerParam('header', ...)`. Request headers
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: rlb-amqp-add-ws-event
3
- description: Add a secure WebSocket event (or HTTP webhook) to the @open-rlb/nestjs-amqp gateway by adding a gateway.events[] entry. Use when the user wants to push broker messages to connected WebSocket clients or to a webhook, with authentication (token in subprotocol), per-event roles/ACL, and per-user scoping to avoid leaking other users' data. Generates the YAML event fragment plus the exchange/queue and ws options, and flags the security wiring.
3
+ description: Add a secure WebSocket event (or HTTP webhook) to the @open-rlb/nestjs-amqp gateway by adding a gateway.events[] entry. Use when the user wants to push broker messages to connected WebSocket clients or to a webhook, with authentication (token in subprotocol), per-event actions/ACL, and per-user scoping to avoid leaking other users' data. Generates the YAML event fragment plus the exchange/queue and ws options, and flags the security wiring.
4
4
  ---
5
5
 
6
6
  # Add a WebSocket / webhook event (gateway.events[])
@@ -27,14 +27,15 @@ each message out to the connected clients of EVERY gateway instance. Secure it b
27
27
  required to subscribe.
28
28
  - `requireAuth: false` → makes `auth` optional (anonymous allowed; claims mapped if a token
29
29
  is present — handy with `scopeClaim`). Defaults to `true` when `auth` is set.
30
- - `roles: [...]` → ACL check (needs `IAclRoleService`); requires `auth` for the identity.
30
+ - `actions: [...]` → ACL action check via `IAclRoleService.checkAction` (needs `IAclRoleService`);
31
+ requires `auth` for the identity. WS events gate **resource-agnostically** (both ids absent).
31
32
  - `scopeClaim` + `payloadKey` → per-user isolation: a client only receives messages where
32
33
  `payload[payloadKey] === claims[scopeClaim]`. `scopeClaim` is the MAPPED claim
33
34
  (with `headerPrefix`, e.g. `X-GTW-AUTH-USERID`). Without `payloadKey` it denies all
34
35
  (gotcha 16). With `auth` but no `scopeClaim`/`payloadKey`, every authorized subscriber
35
36
  gets ALL messages (warned at boot).
36
37
 
37
- > Auth/roles/scope are declared PER-EVENT. `gateway.ws` only holds connection-level limits,
38
+ > Auth/actions/scope are declared PER-EVENT. `gateway.ws` only holds connection-level limits,
38
39
  > heartbeat, origin allowlist and message-size cap (no auth fields). Different events may use
39
40
  > different providers.
40
41
 
@@ -57,7 +58,7 @@ gateway:
57
58
  routingKey: orders.#
58
59
  auth: gateway-jwks # verifies token + maps claims for this event
59
60
  requireAuth: true # default true when auth is set; false → optional
60
- roles: [orders.read] # optional → needs IAclRoleService
61
+ actions: [orders.read] # optional → needs IAclRoleService (checkAction); resource-agnostic for WS
61
62
  scopeClaim: X-GTW-AUTH-USERID # optional per-user scoping (MAPPED claim)
62
63
  payloadKey: userId # message field compared to scopeClaim
63
64
 
@@ -89,8 +90,8 @@ broker:
89
90
  `sample/config-sample/gateway-in-memory/src/main.ts`).
90
91
  - `events[].auth` must reference a `jwt`/`jwks` provider; subscribing without a valid token
91
92
  yields `{ topic:'onError', data:{ event, error:'unauthorized' } }` (unless `requireAuth:false`).
92
- A failed role check yields `error:'forbidden'`.
93
- - `roles` → `IAclRoleService` via `RLB_GTW_ACL_ROLE_SERVICE` in
93
+ A failed action check yields `error:'forbidden'`.
94
+ - `actions` → `IAclRoleService` (`checkAction`) via `RLB_GTW_ACL_ROLE_SERVICE` in
94
95
  `ProxyModule.forRootAsync({ providers: [...] })` (gotcha 15).
95
96
  - Do NOT add a fixed durable queue for the event — the lib creates a per-instance exclusive
96
97
  ephemeral auto-delete queue for fan-out (gotcha 17).
@@ -43,7 +43,21 @@ GatewayAdminModule.forRoot([
43
43
 
44
44
  Use `forRootAsync` to resolve the **consumer-side** `routeDiscovery { exchange, queue }`
45
45
  from config (see Route auto-discovery). Exports `GatewayPathService`,
46
- `GatewayAuthService`, `GatewayMetricsService`.
46
+ `GatewayAuthService`, `GatewayMetricsService`, `GatewayMetricsRollupService`,
47
+ `GatewayHealthService`, `GatewayRetentionService`.
48
+
49
+ **Options also carry `retentionDays`** (default `90` ≈ 3 months; `0`/negative disables) **and
50
+ `rollupRetentionDays`** (default `365` ≈ 1 year; `0`/negative disables rollups). A daily
51
+ `GatewayRetentionService` job prunes the route journal + raw metric points (`retentionDays`, via
52
+ `prune`/`prunePoints`) and the hourly rollups (`rollupRetentionDays`, via `pruneRollups`). An hourly
53
+ `GatewayMetricsRollupService` downsamples the previous hour's raw points into persisted
54
+ `HttpMetricRollup` rows (`recordRollups`) so long-term trends survive raw-point pruning;
55
+ `gw-metrics-rollups` reads them back via `rollupSeries`.
56
+
57
+ **New repo contract methods consumers must implement:** every `*Repository.search(q?, page?, limit?)`
58
+ now returns `Promise<PaginationModel<T>>` (not a bare array); `RouteSyncLogRepository.query(filter,
59
+ page?, limit?)` backs `gw-route-log-search`; `RouteSyncLogRepository.prune` + `HttpMetricRepository.prunePoints`
60
+ back retention.
47
61
 
48
62
  ## Broker topic + queue (required)
49
63
 
@@ -88,10 +102,13 @@ auth/ACL resources. `create` rejects a `(method, path)` collision (409).
88
102
  | --- | --- | --- | --- | --- |
89
103
  | POST | `/admin/paths` | `gw-path-create` | body | needs `name,method,path,topic`; 409 on collision |
90
104
  | GET | `/admin/paths` | `gw-path-list` | query | `?page=&limit=` |
105
+ | GET | `/admin/paths/search` | `gw-path-search` | query | free-text → `PaginationModel<StoredHttpPath>`; `?q=&page=&limit=` |
91
106
  | GET | `/admin/paths/export` | `gw-path-export` | query | enabled, ordered; used by `loadConfig.paths` |
92
107
  | PUT | `/admin/paths` | `gw-path-update` | body | needs `id`; re-checks collisions |
93
108
  | GET | `/admin/paths/get` | `gw-path-get` | query | `?id=` |
94
109
  | DELETE | `/admin/paths` | `gw-path-delete` | body | `{ id }` |
110
+ | GET | `/admin/route-log` | `gw-route-log-list` | query | route journal (who changed what); `?limit=` |
111
+ | GET | `/admin/route-log/search` | `gw-route-log-search` | query | filtered + paginated journal → `PaginationModel<RouteSyncLogEntry>`; `?actor=&service=&event=&routeKey=&from=&to=&page=&limit=` |
95
112
 
96
113
  ## Auth-provider management — `gw-auth-*` (name-keyed PUT-upsert)
97
114
 
@@ -104,6 +121,7 @@ DB-stored providers, ON TOP of the static `auth-providers[]` in YAML.
104
121
  | PUT | `/admin/auth` | `gw-auth-update` | body | upsert by name; `{ name, type, ... }` |
105
122
  | GET | `/admin/auth/get` | `gw-auth-get` | query | `?name=` |
106
123
  | DELETE | `/admin/auth` | `gw-auth-delete` | body | `{ name }` |
124
+ | GET | `/admin/auth/search` | `gw-auth-search` | query | free-text → `PaginationModel<StoredAuthProvider>`; `?q=&page=&limit=` |
107
125
 
108
126
  `gw-auth-export` (dump all enabled) also exists; not in the sample YAML.
109
127
 
@@ -111,12 +129,30 @@ DB-stored providers, ON TOP of the static `auth-providers[]` in YAML.
111
129
 
112
130
  | Method | Path | action | mode | dataSource | Returns |
113
131
  | --- | --- | --- | --- | --- | --- |
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`) |
132
+ | GET | `/admin/metrics` | `gw-metrics-get` | rpc | query | counters/route (`count,errorCount,avgDurationMs,errorRate,lastErrorCode`); `?route=` |
133
+ | GET | `/admin/metrics/series` | `gw-metrics-series` | rpc | query | enriched buckets w/ `p50/p95/p99`+`byStatus`: `?bucketMs=60000&from=&to=&method=&route=&name=` |
134
+ | GET | `/admin/metrics/points` | `gw-metrics-points` | rpc | query | raw points (incl. error `code`) newest-first: `?method=&route=&from=&to=&limit=` |
135
+ | GET | `/admin/metrics/summary` | `gw-metrics-summary` | rpc | query | overview `MetricSummary` (totals,errorRate,p50/95/99,byStatus,top-N): `?from=&to=&method=&route=&name=&topN=10` |
136
+ | GET | `/admin/metrics/prometheus` | `gw-metrics-prometheus` | rpc | query | Prometheus text exposition of counters (`text/plain`) |
137
+ | GET | `/admin/metrics/rollups` | `gw-metrics-rollups` | rpc | query | hourly rollups (`HttpMetricRollup[]`, survive retention): `?from=&to=&method=&route=` |
138
+ | POST | `/admin/metrics/track` | `gw-metrics-track` | event | body | fire-and-forget sink (wired via `gateway.metrics`) |
139
+
140
+ `series`/`summary` are computed **app-side from raw points** (latency percentiles are exact); the
141
+ counters stay O(1) per route. `prometheus` needs the route's `headers: { Content-Type: text/plain }`.
142
+
143
+ `gw-health` is a **readiness** probe (NOT a metrics dump). Returns
144
+ `{ status: 'up'|'down', broker: { status, detail? }, dependencies: { <name>: { status, detail? } } }`;
145
+ `status` is `'down'` if the broker OR any dependency is down. The broker (AmqpConnection) is checked
146
+ built-in; DB/redis/external checks are **consumer-supplied** via `RLB_GW_HEALTH_INDICATORS` (array of
147
+ `GatewayHealthIndicator { name; check(): Promise<{status:'up'|'down', detail?}> }`, both exported from
148
+ `@open-rlb/nestjs-amqp`). The HTTP response is **always 200** (the gateway forwards an rpc result,
149
+ can't set 503) — readiness must inspect `status`.
118
150
 
119
- `gw-health` → `{ status: 'ok' }`, a real 200 liveness probe (NOT a metrics dump):
151
+ ```ts
152
+ { provide: RLB_GW_HEALTH_INDICATORS, useValue: [
153
+ { name: 'database', check: async () => ({ status: 'up' }) },
154
+ ] satisfies GatewayHealthIndicator[] }
155
+ ```
120
156
 
121
157
  ```yaml
122
158
  - name: health
@@ -203,7 +239,7 @@ routes over the same action can publish with different auth — a route with no
203
239
 
204
240
  ```ts
205
241
  @BrokerHTTP('GET', '/admin/bookings/:id', 'params', { name: 'admin-get-booking' })
206
- @BrokerAuth('admin-jwks', undefined, ['admin'], 'admin-get-booking') // pairs by httpName
242
+ @BrokerAuth('admin-jwks', undefined, ['booking.admin'], 'admin-get-booking') // 3rd param = ACL actions; pairs by httpName
207
243
  ```
208
244
 
209
245
  ### Consumer (gateway ← microservice) — `GatewayAdminModule` `routeDiscovery`
@@ -212,8 +248,12 @@ Wired by `GatewayAdminModule` (NOT YAML). Asserts the fanout exchange, subscribe
212
248
  to the durable queue (competing consumers), then per manifest: diffs vs DB scoped
213
249
  to the publishing service, applies only changes (insert/update; soft-disable stale
214
250
  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).
251
+ journals every event via `RouteSyncLogRepository` (each row has `actor:'system'`;
252
+ `skipped` rows mark user-modified routes left untouched; `updated` rows carry a
253
+ `changes` per-field diff like `actions: [+x, -y]`), and publishes `gw-reload` when
254
+ anything changed. Never throws (acks; no poison loop). Stored routes carry `source`
255
+ (`'microservice'|'user'`) and `modified`; user CRUD (`gw-path-*`) is audited too —
256
+ `actor`=`X-GTW-AUTH-USERID` (else `'unknown'`), event `created`/`updated`/`deleted`.
217
257
 
218
258
  ```ts
219
259
  GatewayAdminModule.forRootAsync({
@@ -277,10 +277,10 @@ export class AppService {
277
277
  }
278
278
  ```
279
279
 
280
- Add auth with `@BrokerAuth(authName, allowAnonymous?, roles?, httpName?)` — decoupled from
281
- `@BrokerHTTP`. With one `@BrokerHTTP` it auto-pairs (no `httpName` needed); with multiple,
282
- each `@BrokerHTTP` sets a `name` and each `@BrokerAuth` targets it via `httpName`. A route
283
- with no `@BrokerAuth` is public.
280
+ Add auth with `@BrokerAuth(authName, allowAnonymous?, actions?, httpName?)` — decoupled from
281
+ `@BrokerHTTP` (3rd param is ACL action names). With one `@BrokerHTTP` it auto-pairs (no
282
+ `httpName` needed); with multiple, each `@BrokerHTTP` sets a `name` and each `@BrokerAuth`
283
+ targets it via `httpName`. A route with no `@BrokerAuth` is public.
284
284
 
285
285
  ## Verify
286
286
  - topic/queue/exchange names line up across `broker`/`topics`/paths (gotchas 5–7);