@rhino-dev/rhino-nestjs 4.0.0 → 4.2.0

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 (58) hide show
  1. package/README.md +52 -1
  2. package/dist/controllers/auth.controller.d.ts +34 -5
  3. package/dist/controllers/auth.controller.js +132 -12
  4. package/dist/controllers/auth.controller.js.map +1 -1
  5. package/dist/controllers/invitation.controller.d.ts +41 -2
  6. package/dist/controllers/invitation.controller.js +97 -12
  7. package/dist/controllers/invitation.controller.js.map +1 -1
  8. package/dist/errors/rhino-exception.d.ts +12 -1
  9. package/dist/errors/rhino-exception.js +19 -1
  10. package/dist/errors/rhino-exception.js.map +1 -1
  11. package/dist/guards/group-membership.guard.d.ts +25 -0
  12. package/dist/guards/group-membership.guard.js +70 -0
  13. package/dist/guards/group-membership.guard.js.map +1 -0
  14. package/dist/guards/jwt-auth.guard.js +23 -4
  15. package/dist/guards/jwt-auth.guard.js.map +1 -1
  16. package/dist/index.d.ts +7 -1
  17. package/dist/index.js +17 -2
  18. package/dist/index.js.map +1 -1
  19. package/dist/interfaces/rhino-config.interface.d.ts +71 -0
  20. package/dist/middleware/domain-route-resolver.d.ts +54 -0
  21. package/dist/middleware/domain-route-resolver.js +145 -0
  22. package/dist/middleware/domain-route-resolver.js.map +1 -0
  23. package/dist/middleware/route-group.middleware.d.ts +48 -4
  24. package/dist/middleware/route-group.middleware.js +274 -13
  25. package/dist/middleware/route-group.middleware.js.map +1 -1
  26. package/dist/rhino.config.d.ts +26 -1
  27. package/dist/rhino.config.js +56 -1
  28. package/dist/rhino.config.js.map +1 -1
  29. package/dist/rhino.module.d.ts +7 -0
  30. package/dist/rhino.module.js +23 -1
  31. package/dist/rhino.module.js.map +1 -1
  32. package/dist/services/auth-hooks.service.d.ts +27 -0
  33. package/dist/services/auth-hooks.service.js +85 -0
  34. package/dist/services/auth-hooks.service.js.map +1 -0
  35. package/dist/services/auth.service.d.ts +19 -0
  36. package/dist/services/auth.service.js +80 -5
  37. package/dist/services/auth.service.js.map +1 -1
  38. package/dist/services/invitation.service.d.ts +6 -0
  39. package/dist/services/invitation.service.js +45 -3
  40. package/dist/services/invitation.service.js.map +1 -1
  41. package/dist/services/membership.service.d.ts +56 -0
  42. package/dist/services/membership.service.js +113 -0
  43. package/dist/services/membership.service.js.map +1 -0
  44. package/dist/services/organization.service.js +8 -1
  45. package/dist/services/organization.service.js.map +1 -1
  46. package/dist/services/route-registration.service.d.ts +1 -0
  47. package/dist/services/route-registration.service.js +2 -0
  48. package/dist/services/route-registration.service.js.map +1 -1
  49. package/dist/tsconfig.build.tsbuildinfo +1 -1
  50. package/dist/utils/domain-pattern.d.ts +36 -0
  51. package/dist/utils/domain-pattern.js +77 -0
  52. package/dist/utils/domain-pattern.js.map +1 -0
  53. package/dist/utils/permission-matcher.js +8 -0
  54. package/dist/utils/permission-matcher.js.map +1 -1
  55. package/dist/utils/route-group-validator.d.ts +14 -0
  56. package/dist/utils/route-group-validator.js +124 -0
  57. package/dist/utils/route-group-validator.js.map +1 -0
  58. package/package.json +1 -1
@@ -2,16 +2,60 @@ import { NestMiddleware } from '@nestjs/common';
2
2
  import type { NextFunction, Request, Response } from 'express';
3
3
  import { RhinoConfigService } from '../rhino.config';
4
4
  /**
5
- * Detects which Rhino route group the current request belongs to
6
- * (based on URL prefix) and attaches:
5
+ * Detects which Rhino route group the current request belongs to and attaches:
7
6
  * - `req.__routeGroup` — the group name
8
7
  * - `req.__skipAuth` — true when the group has `skipAuth: true`
9
8
  *
10
- * Install this middleware globally BEFORE `JwtAuthGuard` so the guard can
11
- * read `req.__skipAuth` and short-circuit.
9
+ * Group selection considers BOTH:
10
+ *
11
+ * 1. **Host** — when a group declares a `domain`, it is only eligible for
12
+ * requests whose `req.hostname` matches that domain. A literal domain
13
+ * (`admin.example.com`) must match exactly; a parameterized domain
14
+ * (`{organization}.example.com`) captures the subdomain and exposes it as
15
+ * `req.params.organization` so ResolveOrganizationMiddleware can resolve
16
+ * the tenant from the host — mirroring Laravel's `Route::domain(...)`.
17
+ *
18
+ * 2. **URL prefix** — the existing path-prefix matching, unchanged for groups
19
+ * without a `domain`.
20
+ *
21
+ * Host-scoped groups take precedence over plain prefix groups when both could
22
+ * match, so two groups can share the same prefix and be selected by host.
23
+ *
24
+ * Enforcement: if the request path targets a model that is ONLY served by
25
+ * domain-scoped group(s) and the host matches none of them, the request is
26
+ * rejected with 404 — a wrong-host request must not be served as that group.
27
+ *
28
+ * Install this middleware globally BEFORE `JwtAuthGuard` so the guard can read
29
+ * `req.__skipAuth`, and BEFORE `ResolveOrganizationMiddleware` so a captured
30
+ * subdomain param is available for org resolution.
12
31
  */
13
32
  export declare class RouteGroupMiddleware implements NestMiddleware {
14
33
  private readonly config;
34
+ /** Cache compiled domain patterns keyed by group name. */
35
+ private readonly compiledCache;
15
36
  constructor(config: RhinoConfigService);
37
+ private compileFor;
16
38
  use(req: Request & Record<string, any>, _res: Response, next: NextFunction): void;
39
+ /**
40
+ * Whether `url` targets one of the auth entry endpoints. These are the
41
+ * routes that must never be blocked by the JWT guard (login/register/recover)
42
+ * and that must resolve to the group they belong to. Matches the auth
43
+ * controller's `@Controller('auth')` action paths under any prefix.
44
+ */
45
+ private isAuthEntryPath;
46
+ /**
47
+ * Resolve the route group for an auth entry path (FIX 11.1). Precedence:
48
+ * 1. an auth-enabled DOMAIN group whose host matches (and prefix matches);
49
+ * 2. an auth-enabled PREFIX group whose prefix matches;
50
+ * 3. the empty-prefix / default group (auth-enabled preferred, else any);
51
+ * 4. otherwise leave `__routeGroup` unset (legacy/global auth path).
52
+ *
53
+ * `__skipAuth` is ALWAYS set on an auth entry path so the JWT guard never
54
+ * blocks login/register/recover, regardless of which group wins — even a
55
+ * host-claiming empty-prefix domain group cannot strip it.
56
+ */
57
+ private resolveAuthEntry;
58
+ private resolveHost;
59
+ private prefixMatches;
60
+ private exposeDomainParams;
17
61
  }
@@ -12,45 +12,306 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.RouteGroupMiddleware = void 0;
13
13
  const common_1 = require("@nestjs/common");
14
14
  const rhino_config_1 = require("../rhino.config");
15
+ const domain_pattern_1 = require("../utils/domain-pattern");
15
16
  /**
16
- * Detects which Rhino route group the current request belongs to
17
- * (based on URL prefix) and attaches:
17
+ * Detects which Rhino route group the current request belongs to and attaches:
18
18
  * - `req.__routeGroup` — the group name
19
19
  * - `req.__skipAuth` — true when the group has `skipAuth: true`
20
20
  *
21
- * Install this middleware globally BEFORE `JwtAuthGuard` so the guard can
22
- * read `req.__skipAuth` and short-circuit.
21
+ * Group selection considers BOTH:
22
+ *
23
+ * 1. **Host** — when a group declares a `domain`, it is only eligible for
24
+ * requests whose `req.hostname` matches that domain. A literal domain
25
+ * (`admin.example.com`) must match exactly; a parameterized domain
26
+ * (`{organization}.example.com`) captures the subdomain and exposes it as
27
+ * `req.params.organization` so ResolveOrganizationMiddleware can resolve
28
+ * the tenant from the host — mirroring Laravel's `Route::domain(...)`.
29
+ *
30
+ * 2. **URL prefix** — the existing path-prefix matching, unchanged for groups
31
+ * without a `domain`.
32
+ *
33
+ * Host-scoped groups take precedence over plain prefix groups when both could
34
+ * match, so two groups can share the same prefix and be selected by host.
35
+ *
36
+ * Enforcement: if the request path targets a model that is ONLY served by
37
+ * domain-scoped group(s) and the host matches none of them, the request is
38
+ * rejected with 404 — a wrong-host request must not be served as that group.
39
+ *
40
+ * Install this middleware globally BEFORE `JwtAuthGuard` so the guard can read
41
+ * `req.__skipAuth`, and BEFORE `ResolveOrganizationMiddleware` so a captured
42
+ * subdomain param is available for org resolution.
23
43
  */
24
44
  let RouteGroupMiddleware = class RouteGroupMiddleware {
25
45
  config;
46
+ /** Cache compiled domain patterns keyed by group name. */
47
+ compiledCache = new Map();
26
48
  constructor(config) {
27
49
  this.config = config;
28
50
  }
51
+ compileFor(name, domain) {
52
+ if (this.compiledCache.has(name))
53
+ return this.compiledCache.get(name) ?? null;
54
+ const compiled = (0, domain_pattern_1.compileDomain)(domain);
55
+ this.compiledCache.set(name, compiled);
56
+ return compiled;
57
+ }
29
58
  use(req, _res, next) {
30
59
  // BP-006: `req.url` is stripped to the middleware's mount-point when
31
60
  // registered via NestModule.configure(consumer).forRoutes('*'), so it
32
61
  // collapses to `/` for every request. `req.originalUrl` carries the full
33
62
  // request URL verbatim — use that for prefix matching.
34
63
  const url = String(req.originalUrl ?? req.url ?? '').split('?')[0];
64
+ const host = this.resolveHost(req);
35
65
  const groups = this.config.routeGroups();
36
- let match = null;
66
+ // FIX 11.1 (auth-path resolution): an auth entry path (login/logout/
67
+ // register/password recover|reset) must resolve to the auth-enabled group
68
+ // that matches this host/prefix, falling back to the empty-prefix/default
69
+ // group, and must ALWAYS keep `__skipAuth` so the JWT guard never blocks
70
+ // login/register/recover. A host-claiming empty-prefix DOMAIN group (which
71
+ // matches every path) must NOT win the auth path away from the group it
72
+ // belongs to. This branch is a no-op for apps with no auth-enabled group.
73
+ if (this.isAuthEntryPath(url) && this.config.authEnabledGroups().length > 0) {
74
+ this.resolveAuthEntry(req, url, host, groups);
75
+ return next();
76
+ }
77
+ // NOTE: never mutate `req.__skipAuth` inside this loop. Candidate matches
78
+ // record their group's `skipAuth` flag and the winning match applies it
79
+ // exactly once below, so a losing prefix group can't leak skipAuth onto a
80
+ // domain group that wins precedence (BLOCKER 1).
81
+ let prefixMatch = null;
82
+ let domainMatch = null;
83
+ // The empty-prefix, non-domain "default" group (e.g. `default`), if any. It
84
+ // serves every path that no more-specific prefix/domain group claims, so it
85
+ // is the fallback `__routeGroup` — making enforcement uniform: even the
86
+ // default group tags its routes with the group name (parity with
87
+ // Laravel/Rails). Without this, a null request-group matched ANY membership
88
+ // row (the route_group dimension was ignored). See FIX 3.
89
+ let defaultMatch = null;
90
+ // Tracks whether the URL path targeted a domain-scoped group whose host did
91
+ // NOT match — used for wrong-host 404 enforcement.
92
+ let blockedByHost = false;
93
+ // Tracks whether ANY non-domain group is eligible to serve this request
94
+ // (matched its prefix, or is a catch-all empty-prefix group). An
95
+ // empty-prefix domain group matches every path, so it must not 404 a
96
+ // request that such a non-domain group could serve (BLOCKER 2).
97
+ let nonDomainEligible = false;
37
98
  for (const [name, group] of Object.entries(groups)) {
38
99
  const prefix = group.prefix ?? '';
39
- if (!prefix)
100
+ const compiled = this.compileFor(name, group.domain);
101
+ if (compiled) {
102
+ // Domain-scoped group: only eligible when the host matches.
103
+ const hostResult = (0, domain_pattern_1.matchDomain)(compiled, host);
104
+ const pathMatches = this.prefixMatches(url, prefix);
105
+ if (!hostResult) {
106
+ // Host doesn't match this domain group. If the path would otherwise
107
+ // target this group, remember it for wrong-host enforcement (only
108
+ // honored below when no other group can serve the request).
109
+ if (pathMatches)
110
+ blockedByHost = true;
111
+ continue;
112
+ }
113
+ // Host matches. Require the prefix to match too (empty prefix => match).
114
+ if (!pathMatches)
115
+ continue;
116
+ if (!domainMatch) {
117
+ domainMatch = {
118
+ name,
119
+ prefix,
120
+ params: hostResult.params,
121
+ skipAuth: group.skipAuth,
122
+ };
123
+ }
40
124
  continue;
125
+ }
126
+ // Plain prefix group (no domain) — original behavior.
41
127
  if (prefix.startsWith(':'))
42
128
  continue; // dynamic tenant prefix — handled by ResolveOrganizationMiddleware
43
- if (url.includes(`/${prefix}/`) || url.endsWith(`/${prefix}`)) {
44
- match = { name, prefix };
45
- if (group.skipAuth)
46
- req.__skipAuth = true;
47
- break;
129
+ if (!prefix) {
130
+ // Catch-all (empty-prefix) non-domain group: eligible for any path, so
131
+ // a wrong-host domain group must not block requests it could serve. It
132
+ // becomes the `__routeGroup` only as a FALLBACK (below) — a concrete
133
+ // prefix or domain group always wins. Recording it lets the default
134
+ // group tag its routes with the group name so membership enforcement
135
+ // is uniform across all groups (FIX 3).
136
+ nonDomainEligible = true;
137
+ if (!defaultMatch) {
138
+ defaultMatch = { name, prefix, skipAuth: group.skipAuth };
139
+ }
140
+ continue;
48
141
  }
142
+ if (this.prefixMatches(url, prefix)) {
143
+ nonDomainEligible = true;
144
+ if (!prefixMatch) {
145
+ prefixMatch = { name, prefix, skipAuth: group.skipAuth };
146
+ }
147
+ }
148
+ }
149
+ // Host-scoped match takes precedence over a plain prefix match. Apply
150
+ // `__skipAuth` exactly once, from the winning group only.
151
+ if (domainMatch) {
152
+ req.__routeGroup = domainMatch.name;
153
+ if (domainMatch.skipAuth)
154
+ req.__skipAuth = true;
155
+ // Expose captured subdomain params (e.g. {organization}) so that
156
+ // ResolveOrganizationMiddleware can resolve the tenant from the host.
157
+ this.exposeDomainParams(req, domainMatch.params);
158
+ return next();
159
+ }
160
+ if (prefixMatch) {
161
+ req.__routeGroup = prefixMatch.name;
162
+ if (prefixMatch.skipAuth)
163
+ req.__skipAuth = true;
164
+ return next();
165
+ }
166
+ // Fallback: an empty-prefix, non-domain "default" group serves whatever no
167
+ // concrete prefix/domain group claimed. Tag the request with its name so
168
+ // the default group is a first-class membership dimension (FIX 3). A
169
+ // wrong-host domain group must not pre-empt a request this group can serve,
170
+ // so this also resolves the blockedByHost case below.
171
+ if (defaultMatch) {
172
+ req.__routeGroup = defaultMatch.name;
173
+ if (defaultMatch.skipAuth)
174
+ req.__skipAuth = true;
175
+ return next();
176
+ }
177
+ // No group matched. If a domain-scoped group targeted by the path had a
178
+ // non-matching host AND no non-domain group is eligible to serve the
179
+ // request, reject: a wrong-host request must not be served as that group.
180
+ // (An empty-prefix domain group that would block every path is overridden
181
+ // here by any eligible non-domain group — BLOCKER 2.)
182
+ if (blockedByHost && !nonDomainEligible) {
183
+ return next(new common_1.NotFoundException('Not found'));
49
184
  }
50
- if (match)
51
- req.__routeGroup = match.name;
52
185
  next();
53
186
  }
187
+ /**
188
+ * Whether `url` targets one of the auth entry endpoints. These are the
189
+ * routes that must never be blocked by the JWT guard (login/register/recover)
190
+ * and that must resolve to the group they belong to. Matches the auth
191
+ * controller's `@Controller('auth')` action paths under any prefix.
192
+ */
193
+ isAuthEntryPath(url) {
194
+ return (/(^|\/)auth\/login(\/|$)/.test(url) ||
195
+ /(^|\/)auth\/logout(\/|$)/.test(url) ||
196
+ /(^|\/)auth\/register(\/|$)/.test(url) ||
197
+ /(^|\/)auth\/password\/[^/]+(\/|$)/.test(url));
198
+ }
199
+ /**
200
+ * Resolve the route group for an auth entry path (FIX 11.1). Precedence:
201
+ * 1. an auth-enabled DOMAIN group whose host matches (and prefix matches);
202
+ * 2. an auth-enabled PREFIX group whose prefix matches;
203
+ * 3. the empty-prefix / default group (auth-enabled preferred, else any);
204
+ * 4. otherwise leave `__routeGroup` unset (legacy/global auth path).
205
+ *
206
+ * `__skipAuth` is ALWAYS set on an auth entry path so the JWT guard never
207
+ * blocks login/register/recover, regardless of which group wins — even a
208
+ * host-claiming empty-prefix domain group cannot strip it.
209
+ */
210
+ resolveAuthEntry(req, url, host, groups) {
211
+ let authDomain = null;
212
+ let authPrefix = null;
213
+ let authDefault = null;
214
+ let plainDefault = null;
215
+ for (const [name, group] of Object.entries(groups)) {
216
+ const prefix = group.prefix ?? '';
217
+ if (prefix.startsWith(':'))
218
+ continue; // dynamic tenant prefix
219
+ const authEnabled = group.auth === true && name !== 'public';
220
+ const compiled = this.compileFor(name, group.domain);
221
+ if (compiled) {
222
+ if (!authEnabled)
223
+ continue;
224
+ const hostResult = (0, domain_pattern_1.matchDomain)(compiled, host);
225
+ if (!hostResult)
226
+ continue;
227
+ if (!this.prefixMatches(url, prefix))
228
+ continue;
229
+ if (!authDomain)
230
+ authDomain = { name, params: hostResult.params };
231
+ continue;
232
+ }
233
+ // Plain (non-domain) group.
234
+ if (!prefix) {
235
+ // Empty-prefix / default group: the auth-path fallback.
236
+ if (authEnabled) {
237
+ if (authDefault == null)
238
+ authDefault = name;
239
+ }
240
+ else if (plainDefault == null) {
241
+ plainDefault = name;
242
+ }
243
+ continue;
244
+ }
245
+ if (authEnabled && this.prefixMatches(url, prefix)) {
246
+ // Prefer the most specific (longest) matching prefix.
247
+ if (!authPrefix || prefix.length > authPrefix.prefixLen) {
248
+ authPrefix = { name, prefixLen: prefix.length };
249
+ }
250
+ }
251
+ }
252
+ // Auth entry paths are always exempt from the JWT guard.
253
+ req.__skipAuth = true;
254
+ if (authDomain) {
255
+ req.__routeGroup = authDomain.name;
256
+ this.exposeDomainParams(req, authDomain.params);
257
+ return;
258
+ }
259
+ if (authPrefix) {
260
+ req.__routeGroup = authPrefix.name;
261
+ return;
262
+ }
263
+ // Fall back to the default group: an auth-enabled empty-prefix group is the
264
+ // legacy/global auth path itself (design §11.1), so it adopts the auth
265
+ // route. If none is auth-enabled, use any empty-prefix default group so the
266
+ // request still carries a first-class group (parity with FIX 3).
267
+ const fallback = authDefault ?? plainDefault;
268
+ if (fallback)
269
+ req.__routeGroup = fallback;
270
+ // else: no group claims it → legacy/global auth path (null group).
271
+ }
272
+ resolveHost(req) {
273
+ // Express populates `req.hostname` (port-stripped). Fall back to the Host
274
+ // header for environments/tests that only set headers.
275
+ const fromExpress = req.hostname;
276
+ if (fromExpress)
277
+ return String(fromExpress);
278
+ const header = req.headers?.host ?? req.host;
279
+ return header ? String(header).split(':')[0] : undefined;
280
+ }
281
+ prefixMatches(url, prefix) {
282
+ if (!prefix)
283
+ return true; // empty prefix => any path under the group
284
+ if (prefix.startsWith(':'))
285
+ return false;
286
+ return url.includes(`/${prefix}/`) || url.endsWith(`/${prefix}`);
287
+ }
288
+ exposeDomainParams(req, params) {
289
+ if (!params || Object.keys(params).length === 0)
290
+ return;
291
+ // DNS hostnames are case-insensitive but the regex capture preserves the
292
+ // host's casing verbatim. Lowercase the captured subdomain params so org
293
+ // resolution (and the resolver's cache key) is consistent — `ACME` and
294
+ // `acme` must resolve the same lowercase-slugged org.
295
+ const normalized = {};
296
+ for (const [key, value] of Object.entries(params)) {
297
+ normalized[key] = typeof value === 'string' ? value.toLowerCase() : value;
298
+ }
299
+ req.params = req.params ?? {};
300
+ for (const [key, value] of Object.entries(normalized)) {
301
+ // Don't clobber an explicit path param of the same name.
302
+ if (req.params[key] == null)
303
+ req.params[key] = value;
304
+ }
305
+ // Convention: a `{organization}` (or `{org}`) domain param feeds org
306
+ // resolution exactly like the `:organization` path param does. Surface it
307
+ // under `organization` so ResolveOrganizationMiddleware reads it uniformly.
308
+ const orgParam = normalized.organization ?? normalized.org;
309
+ if (orgParam != null && req.params.organization == null) {
310
+ req.params.organization = orgParam;
311
+ }
312
+ // Also record the (normalized) captured subdomain for downstream/debug use.
313
+ req.__domainParams = normalized;
314
+ }
54
315
  };
55
316
  exports.RouteGroupMiddleware = RouteGroupMiddleware;
56
317
  exports.RouteGroupMiddleware = RouteGroupMiddleware = __decorate([
@@ -1 +1 @@
1
- {"version":3,"file":"route-group.middleware.js","sourceRoot":"","sources":["../../src/middleware/route-group.middleware.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4D;AAE5D,kDAAqD;AAErD;;;;;;;;GAQG;AAEI,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IACF;IAA7B,YAA6B,MAA0B;QAA1B,WAAM,GAAN,MAAM,CAAoB;IAAG,CAAC;IAE3D,GAAG,CAAC,GAAkC,EAAE,IAAc,EAAE,IAAkB;QACxE,qEAAqE;QACrE,sEAAsE;QACtE,yEAAyE;QACzE,uDAAuD;QACvD,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACnE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QACzC,IAAI,KAAK,GAA4C,IAAI,CAAC;QAC1D,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACnD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC;YAClC,IAAI,CAAC,MAAM;gBAAE,SAAS;YACtB,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,SAAS,CAAC,mEAAmE;YACzG,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,MAAM,GAAG,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,MAAM,EAAE,CAAC,EAAE,CAAC;gBAC9D,KAAK,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;gBACzB,IAAI,KAAK,CAAC,QAAQ;oBAAE,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC;gBAC1C,MAAM;YACR,CAAC;QACH,CAAC;QACD,IAAI,KAAK;YAAE,GAAG,CAAC,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC;QACzC,IAAI,EAAE,CAAC;IACT,CAAC;CACF,CAAA;AAxBY,oDAAoB;+BAApB,oBAAoB;IADhC,IAAA,mBAAU,GAAE;qCAE0B,iCAAkB;GAD5C,oBAAoB,CAwBhC"}
1
+ {"version":3,"file":"route-group.middleware.js","sourceRoot":"","sources":["../../src/middleware/route-group.middleware.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
- import type { RhinoConfig, ModelRegistration, RouteGroupConfig } from './interfaces/rhino-config.interface';
1
+ import type { RhinoConfig, ModelRegistration, RouteGroupConfig, AuthLifecycleHooks } from './interfaces/rhino-config.interface';
2
+ import type { Type } from '@nestjs/common';
2
3
  /**
3
4
  * Injectable accessor for the consuming app's Rhino configuration.
4
5
  * Wraps the raw config object with convenience lookup methods.
@@ -17,6 +18,29 @@ export declare class RhinoConfigService {
17
18
  * `'*'` expands to every registered model.
18
19
  */
19
20
  modelsInRouteGroup(name: string): string[];
21
+ /**
22
+ * Master flag for group-membership enforcement (Decision 9.A/B/C). Default
23
+ * `false` → behavior unchanged.
24
+ */
25
+ enforceGroupMembership(): boolean;
26
+ /** Whether a group has opted into per-group auth routes (Decision 9.A). */
27
+ routeGroupAuthEnabled(name: string | null | undefined): boolean;
28
+ /** The configured lifecycle-hooks provider/object for a group, if any. */
29
+ routeGroupHooks(name: string | null | undefined): Type<AuthLifecycleHooks> | AuthLifecycleHooks | undefined;
30
+ /** Names of all groups with `auth: true` (excludes the `public` group). */
31
+ authEnabledGroups(): string[];
32
+ /**
33
+ * Whether a route group is a tenant (org-scoped) group. A group is a tenant
34
+ * group when multi-tenancy is enabled AND the group does not opt out via
35
+ * `belongsToOrganization: false`-style config; here we treat a group as a
36
+ * tenant group when its declared models include any org-scoped model, or when
37
+ * the group carries a tenant `domain`/`prefix` param. Conservatively: a group
38
+ * is non-tenant only when explicitly marked. For membership purposes the org
39
+ * is only required when the request actually resolved an organization, so the
40
+ * effective rule is "tenant group ⇒ org must match". We expose the simpler
41
+ * predicate: multi-tenant enabled and not the public group.
42
+ */
43
+ isTenantGroup(name: string | null | undefined): boolean;
20
44
  multiTenantEnabled(): boolean;
21
45
  organizationIdentifierColumn(): string;
22
46
  nestedConfig(): {
@@ -35,6 +59,7 @@ export declare class RhinoConfigService {
35
59
  userModel: string;
36
60
  emailField: string;
37
61
  passwordField: string;
62
+ enforceGroupMembership: boolean;
38
63
  };
39
64
  }
40
65
  /**
@@ -16,6 +16,7 @@ exports.RhinoConfigService = void 0;
16
16
  exports.normalizeConfig = normalizeConfig;
17
17
  const common_1 = require("@nestjs/common");
18
18
  const tokens_1 = require("./constants/tokens");
19
+ const route_group_validator_1 = require("./utils/route-group-validator");
19
20
  /**
20
21
  * Injectable accessor for the consuming app's Rhino configuration.
21
22
  * Wraps the raw config object with convenience lookup methods.
@@ -55,6 +56,54 @@ let RhinoConfigService = class RhinoConfigService {
55
56
  return Object.keys(this.models());
56
57
  return group.models;
57
58
  }
59
+ /**
60
+ * Master flag for group-membership enforcement (Decision 9.A/B/C). Default
61
+ * `false` → behavior unchanged.
62
+ */
63
+ enforceGroupMembership() {
64
+ return this.config.auth?.enforceGroupMembership === true;
65
+ }
66
+ /** Whether a group has opted into per-group auth routes (Decision 9.A). */
67
+ routeGroupAuthEnabled(name) {
68
+ if (!name)
69
+ return false;
70
+ return this.routeGroup(name)?.auth === true;
71
+ }
72
+ /** The configured lifecycle-hooks provider/object for a group, if any. */
73
+ routeGroupHooks(name) {
74
+ if (!name)
75
+ return undefined;
76
+ return this.routeGroup(name)?.hooks;
77
+ }
78
+ /** Names of all groups with `auth: true` (excludes the `public` group). */
79
+ authEnabledGroups() {
80
+ return Object.entries(this.routeGroups())
81
+ .filter(([name, g]) => g.auth === true && name !== 'public')
82
+ .map(([name]) => name);
83
+ }
84
+ /**
85
+ * Whether a route group is a tenant (org-scoped) group. A group is a tenant
86
+ * group when multi-tenancy is enabled AND the group does not opt out via
87
+ * `belongsToOrganization: false`-style config; here we treat a group as a
88
+ * tenant group when its declared models include any org-scoped model, or when
89
+ * the group carries a tenant `domain`/`prefix` param. Conservatively: a group
90
+ * is non-tenant only when explicitly marked. For membership purposes the org
91
+ * is only required when the request actually resolved an organization, so the
92
+ * effective rule is "tenant group ⇒ org must match". We expose the simpler
93
+ * predicate: multi-tenant enabled and not the public group.
94
+ */
95
+ isTenantGroup(name) {
96
+ if (name === 'public')
97
+ return false;
98
+ if (name) {
99
+ const group = this.routeGroup(name);
100
+ // Explicit per-group override wins.
101
+ if (group && typeof group.tenant === 'boolean')
102
+ return group.tenant;
103
+ }
104
+ // Default: a group is org-scoped iff multi-tenancy is enabled.
105
+ return this.multiTenantEnabled();
106
+ }
58
107
  multiTenantEnabled() {
59
108
  const mt = this.config.multiTenant;
60
109
  if (!mt)
@@ -89,6 +138,7 @@ let RhinoConfigService = class RhinoConfigService {
89
138
  userModel: this.config.auth?.userModel ?? 'user',
90
139
  emailField: this.config.auth?.emailField ?? 'email',
91
140
  passwordField: this.config.auth?.passwordField ?? 'password',
141
+ enforceGroupMembership: this.config.auth?.enforceGroupMembership === true,
92
142
  };
93
143
  }
94
144
  };
@@ -102,7 +152,7 @@ exports.RhinoConfigService = RhinoConfigService = __decorate([
102
152
  * Normalize a raw config value (defaults applied) — used by the module in forRoot.
103
153
  */
104
154
  function normalizeConfig(config) {
105
- return {
155
+ const normalized = {
106
156
  ...config,
107
157
  models: config.models ?? {},
108
158
  routeGroups: config.routeGroups ?? {},
@@ -123,8 +173,13 @@ function normalizeConfig(config) {
123
173
  userModel: 'user',
124
174
  emailField: 'email',
125
175
  passwordField: 'password',
176
+ enforceGroupMembership: false,
126
177
  ...(config.auth ?? {}),
127
178
  },
128
179
  };
180
+ // Fail fast on route groups that would silently shadow each other (same
181
+ // prefix + intersecting host-set + overlapping models).
182
+ (0, route_group_validator_1.validateRouteGroups)(normalized);
183
+ return normalized;
129
184
  }
130
185
  //# sourceMappingURL=rhino.config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rhino.config.js","sourceRoot":"","sources":["../src/rhino.config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"rhino.config.js","sourceRoot":"","sources":["../src/rhino.config.ts"],"names":[],"mappings":"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"}
@@ -13,6 +13,12 @@ export interface RhinoModuleOptions {
13
13
  autoModelMiddleware?: boolean;
14
14
  /** Wire ResolveOrganizationMiddleware for all routes when multiTenant enabled. Default true. */
15
15
  autoTenantMiddleware?: boolean;
16
+ /**
17
+ * Install GroupMembershipGuard globally as APP_GUARD. Default false (opt-in).
18
+ * The guard is a no-op unless `auth.enforceGroupMembership` is on, so this is
19
+ * safe to enable alongside `autoAuthGuard` when using group membership.
20
+ */
21
+ autoMembershipGuard?: boolean;
16
22
  }
17
23
  /**
18
24
  * Rhino NestJS dynamic module.
@@ -57,5 +63,6 @@ export declare class RhinoModule implements NestModule {
57
63
  private static build;
58
64
  private static collectModelMiddleware;
59
65
  private static coreProviders;
66
+ private static collectHookClasses;
60
67
  private static coreExports;
61
68
  }