@happyvertical/smrt-users 0.38.7 → 0.38.9

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.
@@ -4,6 +4,16 @@ import { RolePermissionPatternMatrix, SeedRolePermissionsOptions } from './RoleP
4
4
  export interface SeedSystemRolesOptions extends Omit<SeedRolePermissionsOptions, 'catalog'> {
5
5
  permissionMatrix?: RolePermissionPatternMatrix;
6
6
  seedPermissions?: boolean;
7
+ /**
8
+ * System role slugs to flag `inheritsToDescendants: true` (opt-in
9
+ * hierarchical membership inheritance, e.g. `['owner', 'admin']` for
10
+ * organizational hierarchies). Additive and idempotent: listed slugs are
11
+ * flagged on create and on already-seeded roles, but omitting a slug never
12
+ * unsets a previously flagged role — unflag explicitly via a role update.
13
+ * Unknown slugs throw (a typo here would silently withhold intended
14
+ * authority). Default: no role inherits.
15
+ */
16
+ inheritsToDescendants?: string[];
7
17
  }
8
18
  /**
9
19
  * Collection for managing Role objects
@@ -1 +1 @@
1
- {"version":3,"file":"RoleCollection.d.ts","sourceRoot":"","sources":["../../src/collections/RoleCollection.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAEzC,OAAO,KAAK,EACV,2BAA2B,EAC3B,0BAA0B,EAC3B,MAAM,+BAA+B,CAAC;AAEvC,MAAM,WAAW,sBACf,SAAQ,IAAI,CAAC,0BAA0B,EAAE,SAAS,CAAC;IACnD,gBAAgB,CAAC,EAAE,2BAA2B,CAAC;IAC/C,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,cAAc,CAAC,IAAI,CAAC;IACtD,MAAM,CAAC,QAAQ,CAAC,UAAU,cAAQ;IAElC;;OAEG;IACG,eAAe,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;IAOxC;;OAEG;IACG,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IASrD;;OAEG;IACG,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAOxD;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAoBvE;;OAEG;IACG,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;CAuC7E"}
1
+ {"version":3,"file":"RoleCollection.d.ts","sourceRoot":"","sources":["../../src/collections/RoleCollection.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAEzC,OAAO,KAAK,EACV,2BAA2B,EAC3B,0BAA0B,EAC3B,MAAM,+BAA+B,CAAC;AAEvC,MAAM,WAAW,sBACf,SAAQ,IAAI,CAAC,0BAA0B,EAAE,SAAS,CAAC;IACnD,gBAAgB,CAAC,EAAE,2BAA2B,CAAC;IAC/C,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;;OAQG;IACH,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;CAClC;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,cAAc,CAAC,IAAI,CAAC;IACtD,MAAM,CAAC,QAAQ,CAAC,UAAU,cAAQ;IAElC;;OAEG;IACG,eAAe,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;IAOxC;;OAEG;IACG,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IASrD;;OAEG;IACG,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAOxD;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAoBvE;;OAEG;IACG,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;CA0D7E"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as UsersCliAuthRequestCollection, A as DEFAULT_ROLE_PERMISSION_PATTERNS, B as Role, C as TenantHierarchyError, D as DEFAULT_SESSION_TTL, E as SessionCollection, F as normalizeOperationPermissionAction, G as MembershipCollection, H as Permission, I as registerPermissionDefinitions, J as GroupRole, K as Membership, L as syncPermissionCatalog, M as PermissionCatalogService, N as deriveOperationPermissionCollectionName, O as Session, P as deriveOperationPermissionSlug, Q as Group, R as RolePermission, S as TenantCollection, T as Tenant, U as MembershipOverrideCollection, V as PermissionCollection, W as MembershipOverride, X as GroupMember, Y as GroupMemberCollection, Z as GroupCollection, _ as resolveOidcProviderConfig, a as TerminalAuthRateLimitError, at as DEFAULT_ROLES, b as TenantPermissionOverrideCollection, c as getRequestScopedDatabase, ct as MembershipStatus, d as PermissionResolver, dt as TenantPermissionEffect, et as UsersCliAuthRequest, f as OidcLoginError, ft as TenantStatus, g as getUsersOidcConfig, h as encodeOidcTransaction, i as TerminalAuthError, it as AccessRequestStatus, j as RolePermissionCollection, k as generateSessionId, l as withSessionPermissionContext, lt as OverrideEffect, m as decodeOidcTransaction, n as DEFAULT_CLI_AUTH_REQUEST_TTL_SECONDS, nt as isValidEmail, o as TerminalAuthService, ot as DEFAULT_ROLE_SLUGS, p as OidcLoginService, pt as UserStatus, q as GroupRoleCollection, r as DEFAULT_CLI_SESSION_TTL_SECONDS, rt as normalizeEmail, s as getCurrentSessionPermissionContext, st as DEFAULT_TENANT_POLICY, t as DEFAULT_CLI_AUTH_POLL_INTERVAL_SECONDS, tt as User, u as SessionService, ut as SessionStatus, w as MAX_TENANT_HIERARCHY_DEPTH, x as TenantPermissionOverride, y as UserCollection, z as RoleCollection } from "./chunks/TerminalAuthService-PONT33zt.js";
1
+ import { $ as UsersCliAuthRequestCollection, A as DEFAULT_ROLE_PERMISSION_PATTERNS, B as Role, C as TenantHierarchyError, D as DEFAULT_SESSION_TTL, E as SessionCollection, F as normalizeOperationPermissionAction, G as MembershipCollection, H as Permission, I as registerPermissionDefinitions, J as GroupRole, K as Membership, L as syncPermissionCatalog, M as PermissionCatalogService, N as deriveOperationPermissionCollectionName, O as Session, P as deriveOperationPermissionSlug, Q as Group, R as RolePermission, S as TenantCollection, T as Tenant, U as MembershipOverrideCollection, V as PermissionCollection, W as MembershipOverride, X as GroupMember, Y as GroupMemberCollection, Z as GroupCollection, _ as resolveOidcProviderConfig, a as TerminalAuthRateLimitError, at as DEFAULT_ROLES, b as TenantPermissionOverrideCollection, c as getRequestScopedDatabase, ct as MembershipStatus, d as PermissionResolver, dt as TenantPermissionEffect, et as UsersCliAuthRequest, f as OidcLoginError, ft as TenantStatus, g as getUsersOidcConfig, h as encodeOidcTransaction, i as TerminalAuthError, it as AccessRequestStatus, j as RolePermissionCollection, k as generateSessionId, l as withSessionPermissionContext, lt as OverrideEffect, m as decodeOidcTransaction, n as DEFAULT_CLI_AUTH_REQUEST_TTL_SECONDS, nt as isValidEmail, o as TerminalAuthService, ot as DEFAULT_ROLE_SLUGS, p as OidcLoginService, pt as UserStatus, q as GroupRoleCollection, r as DEFAULT_CLI_SESSION_TTL_SECONDS, rt as normalizeEmail, s as getCurrentSessionPermissionContext, st as DEFAULT_TENANT_POLICY, t as DEFAULT_CLI_AUTH_POLL_INTERVAL_SECONDS, tt as User, u as SessionService, ut as SessionStatus, w as MAX_TENANT_HIERARCHY_DEPTH, x as TenantPermissionOverride, y as UserCollection, z as RoleCollection } from "./chunks/TerminalAuthService-BgwNOEPM.js";
2
2
  import { ObjectRegistry, SmrtCollection, SmrtObject, field, findManifestEntryByQualifiedName, smrt } from "@happyvertical/smrt-core";
3
3
  import { createLogger } from "@happyvertical/logger";
4
4
  import { createHash } from "node:crypto";
@@ -736,7 +736,7 @@ var MagicLinkService = class MagicLinkService {
736
736
  * The caller is responsible for emailing the token to the user.
737
737
  */
738
738
  async generate(email) {
739
- const { SignJWT } = await import("./chunks/TerminalAuthService-PONT33zt.js").then((n) => n.v);
739
+ const { SignJWT } = await import("./chunks/TerminalAuthService-BgwNOEPM.js").then((n) => n.v);
740
740
  const key = await this.getSigningKey();
741
741
  const nonce = crypto.randomUUID();
742
742
  const normalizedEmail = normalizeEmail(email);
@@ -765,7 +765,7 @@ var MagicLinkService = class MagicLinkService {
765
765
  * @throws {MagicLinkError} If the token is invalid, expired, or already used
766
766
  */
767
767
  async verify(token) {
768
- const { jwtVerify, errors } = await import("./chunks/TerminalAuthService-PONT33zt.js").then((n) => n.v);
768
+ const { jwtVerify, errors } = await import("./chunks/TerminalAuthService-BgwNOEPM.js").then((n) => n.v);
769
769
  const key = await this.getSigningKey();
770
770
  let payload;
771
771
  try {
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "version": "1.0.0",
3
- "timestamp": 1783286612374,
3
+ "timestamp": 1783363375031,
4
4
  "packageName": "@happyvertical/smrt-users",
5
- "packageVersion": "0.38.7",
5
+ "packageVersion": "0.38.9",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-users:AccessRequestCollection": {
8
8
  "name": "accessrequestcollection",
@@ -4123,6 +4123,11 @@
4123
4123
  "type": "boolean",
4124
4124
  "required": false,
4125
4125
  "default": false
4126
+ },
4127
+ "inheritsToDescendants": {
4128
+ "type": "boolean",
4129
+ "required": false,
4130
+ "default": false
4126
4131
  }
4127
4132
  },
4128
4133
  "methods": {
@@ -4179,7 +4184,7 @@
4179
4184
  "collectionExportName": "RoleCollection",
4180
4185
  "schema": {
4181
4186
  "tableName": "roles",
4182
- "ddl": "CREATE TABLE IF NOT EXISTS \"roles\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" UUID,\n \"name\" TEXT DEFAULT '',\n \"description\" TEXT DEFAULT '',\n \"is_system\" BOOLEAN DEFAULT FALSE\n);",
4187
+ "ddl": "CREATE TABLE IF NOT EXISTS \"roles\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" UUID,\n \"name\" TEXT DEFAULT '',\n \"description\" TEXT DEFAULT '',\n \"is_system\" BOOLEAN DEFAULT FALSE,\n \"inherits_to_descendants\" BOOLEAN DEFAULT FALSE\n);",
4183
4188
  "columns": {
4184
4189
  "id": {
4185
4190
  "type": "UUID",
@@ -4229,6 +4234,12 @@
4229
4234
  "notNull": false,
4230
4235
  "unique": false,
4231
4236
  "default": false
4237
+ },
4238
+ "inherits_to_descendants": {
4239
+ "type": "BOOLEAN",
4240
+ "notNull": false,
4241
+ "unique": false,
4242
+ "default": false
4232
4243
  }
4233
4244
  },
4234
4245
  "indexes": [
@@ -4247,7 +4258,7 @@
4247
4258
  "unique": true
4248
4259
  }
4249
4260
  ],
4250
- "version": "7f823f30"
4261
+ "version": "6c08582e"
4251
4262
  }
4252
4263
  },
4253
4264
  "@happyvertical/smrt-users:RolePermission": {
@@ -8,6 +8,7 @@ export interface RoleOptions extends SmrtObjectOptions {
8
8
  name?: string;
9
9
  description?: string;
10
10
  isSystem?: boolean;
11
+ inheritsToDescendants?: boolean;
11
12
  }
12
13
  /**
13
14
  * Role represents a permission template that can be assigned to users.
@@ -52,6 +53,16 @@ export declare class Role extends SmrtObject implements RoleContract {
52
53
  * Whether this is a system role (cannot be deleted)
53
54
  */
54
55
  isSystem: boolean;
56
+ /**
57
+ * Opt-in hierarchical inheritance: when true, an ACTIVE membership holding
58
+ * this role also resolves permissions in descendant tenants where the user
59
+ * has no direct membership (nearest flagged ancestor membership wins).
60
+ *
61
+ * Default false: the role's authority is exact-tenant only, which is the
62
+ * pre-existing resolver behavior. Flag structural roles like a network-level
63
+ * owner/admin; leave per-tenant roles like member/viewer unflagged.
64
+ */
65
+ inheritsToDescendants: boolean;
55
66
  constructor(options?: RoleOptions);
56
67
  /**
57
68
  * Check if this is a system-wide role
@@ -1 +1 @@
1
- {"version":3,"file":"Role.d.ts","sourceRoot":"","sources":["../../src/models/Role.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAEL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,IAAI,IAAI,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAEtE;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,iBAAiB;IACpD,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAOa,IAAK,SAAQ,UAAW,YAAW,YAAY;IAC1D;;;OAGG;IAEH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAEzB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAM;IAElB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAM;IAEzB;;OAEG;IACH,QAAQ,EAAE,OAAO,CAAS;gBAEd,OAAO,GAAE,WAAgB;IASrC;;OAEG;IACH,YAAY,IAAI,OAAO;IAIvB;;OAEG;IACH,YAAY,IAAI,OAAO;IAIvB;;;;OAIG;IACH,SAAS,IAAI,OAAO;IAIpB;;;OAGG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAQ9B"}
1
+ {"version":3,"file":"Role.d.ts","sourceRoot":"","sources":["../../src/models/Role.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAEL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,IAAI,IAAI,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAEtE;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,iBAAiB;IACpD,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAOa,IAAK,SAAQ,UAAW,YAAW,YAAY;IAC1D;;;OAGG;IAEH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAEzB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAM;IAElB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAM;IAEzB;;OAEG;IACH,QAAQ,EAAE,OAAO,CAAS;IAE1B;;;;;;;;OAQG;IACH,qBAAqB,EAAE,OAAO,CAAS;gBAE3B,OAAO,GAAE,WAAgB;IAWrC;;OAEG;IACH,YAAY,IAAI,OAAO;IAIvB;;OAEG;IACH,YAAY,IAAI,OAAO;IAIvB;;;;OAIG;IACH,SAAS,IAAI,OAAO;IAIpB;;;OAGG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAQ9B"}
@@ -15,12 +15,25 @@ export interface PermissionResolutionResult {
15
15
  groupIds: string[];
16
16
  /** Permission IDs explicitly denied */
17
17
  deniedPermissionIds: string[];
18
+ /**
19
+ * Ancestor tenant id the resolving membership belongs to, when resolution
20
+ * used opt-in hierarchical inheritance (no direct membership in the target
21
+ * tenant; nearest ACTIVE ancestor membership whose role has
22
+ * `inheritsToDescendants: true`). `null` for direct-membership resolution.
23
+ */
24
+ inheritedFromTenantId: string | null;
18
25
  }
19
26
  export interface PermissionResolutionOptions {
20
27
  /**
21
- * Active membership already resolved by the caller for this user/tenant.
28
+ * Membership row already resolved by the caller for this user/tenant.
22
29
  * Passing this lets request-scoped session loaders avoid re-querying the
23
30
  * same membership row before resolving permissions.
31
+ *
32
+ * Pass the RAW lookup result: an inactive row pins resolution to the empty
33
+ * set (a suspended/pending direct membership stays effective), while an
34
+ * explicit `null` asserts "no direct membership row exists" and makes the
35
+ * resolver consider opt-in ancestor-membership inheritance. Leaving it
36
+ * `undefined` lets the resolver perform its own lookup.
24
37
  */
25
38
  membership?: Membership | null;
26
39
  }
@@ -71,6 +84,17 @@ export interface TenantPermissionInheritanceResult {
71
84
  * - GRANT: Explicitly grant at this level
72
85
  * - DENY: Explicitly block (even if parent grants)
73
86
  *
87
+ * ## Hierarchical Membership-Role Inheritance (opt-in)
88
+ *
89
+ * Independent of the tenant-level cascade above, membership-role authority
90
+ * can follow the hierarchy DOWN when a role is explicitly flagged
91
+ * `inheritsToDescendants: true`: a user with no direct membership in the
92
+ * target tenant resolves through the nearest ACTIVE ancestor membership
93
+ * holding such a role. A direct membership row in the target tenant always
94
+ * wins (including inactive rows, which resolve empty), child tenant-DENY
95
+ * overrides still subtract from inherited grants, and with no flagged role
96
+ * the resolver behaves exactly as before. See `resolvePermissions`.
97
+ *
74
98
  * @example
75
99
  * ```typescript
76
100
  * const resolver = new PermissionResolver(options);
@@ -90,6 +114,7 @@ export interface TenantPermissionInheritanceResult {
90
114
  export declare class PermissionResolver {
91
115
  private options;
92
116
  private membershipCollection;
117
+ private roleCollection;
93
118
  private rolePermissionCollection;
94
119
  private membershipOverrideCollection;
95
120
  private groupMemberCollection;
@@ -138,8 +163,29 @@ export declare class PermissionResolver {
138
163
  * -> membership GRANT (re-adds; most specific, can win over a tenant-DENY)
139
164
  * -> membership DENY (absolute; always wins)
140
165
  *
166
+ * ## Membership selection
167
+ *
168
+ * A direct membership row in the target tenant always pins resolution to
169
+ * itself: active rows resolve normally, inactive (pending/suspended) rows
170
+ * resolve to the empty set — an explicit direct membership is authoritative
171
+ * even when it attenuates or suspends a user who holds broader authority on
172
+ * an ancestor tenant.
173
+ *
174
+ * Only when NO direct membership row exists does the resolver consider
175
+ * opt-in hierarchical inheritance: it walks the tenant's ancestors (nearest
176
+ * first) and resolves through the nearest ACTIVE ancestor membership whose
177
+ * role has `inheritsToDescendants: true`. All later layers then run
178
+ * unchanged against the TARGET tenant — the tenant cascade and tenant-DENY
179
+ * block come from the target tenant (so a child tenant can still carve
180
+ * authority out of an inherited role), group roles remain exact-tenant
181
+ * (only groups the user belongs to in the target tenant contribute), and
182
+ * membership GRANT/DENY overrides travel with the ancestor membership used.
183
+ * With no role flagged `inheritsToDescendants`, resolution is identical to
184
+ * the pre-inheritance behavior.
185
+ *
141
186
  * Algorithm:
142
- * 1. Get membership and collect all permission IDs from all sources
187
+ * 1. Get membership (direct, or nearest inheritable ancestor membership)
188
+ * and collect all permission IDs from all sources
143
189
  * 2. Batch fetch all permissions in a single query
144
190
  * 3. Apply permissions from role, then groups
145
191
  * 4. Subtract tenant-level DENY'd slugs (hard tenant-wide block)
@@ -147,6 +193,27 @@ export declare class PermissionResolver {
147
193
  * 6. Subtract membership DENY overrides (absolute precedence)
148
194
  */
149
195
  resolvePermissions(userId: string, tenantId: string, options?: PermissionResolutionOptions): Promise<PermissionResolutionResult>;
196
+ /**
197
+ * Find the membership to resolve through when the user has no direct
198
+ * membership row in the target tenant.
199
+ *
200
+ * Walks the target tenant's ancestor chain (from the materialized
201
+ * `hierarchyPath`, nearest ancestor first) and returns the nearest ACTIVE
202
+ * ancestor membership whose role is explicitly flagged
203
+ * `inheritsToDescendants: true`. Ancestor memberships that are inactive or
204
+ * hold an unflagged role are skipped — they neither confer nor block
205
+ * inheritance from higher ancestors. Attenuating a user in a specific
206
+ * tenant is expressed with a direct membership row there (which pins
207
+ * resolution) or a tenant-level DENY, not with an intermediate unflagged
208
+ * membership.
209
+ *
210
+ * Fails closed (returns null, resolving to the empty set) when the tenant
211
+ * is missing or its `hierarchyPath` is malformed: deeper than
212
+ * `MAX_TENANT_HIERARCHY_DEPTH`, self-referential, containing duplicate
213
+ * ancestor ids, or inconsistent with the actual `parentTenantId` chain
214
+ * (e.g. a stale path naming an unrelated tenant).
215
+ */
216
+ private resolveInheritedMembership;
150
217
  /**
151
218
  * Check if a user has a specific permission in a tenant
152
219
  */
@@ -1 +1 @@
1
- {"version":3,"file":"PermissionResolver.d.ts","sourceRoot":"","sources":["../../src/services/PermissionResolver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PermissionResolver.d.ts","sourceRoot":"","sources":["../../src/services/PermissionResolver.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"SessionService.d.ts","sourceRoot":"","sources":["../../src/services/SessionService.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE,OAAO,EACL,KAAK,oBAAoB,EAE1B,MAAM,qCAAqC,CAAC;AAE7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAuB,KAAK,OAAO,EAAE,MAAM,sBAAsB,CAAC;AACzE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAG9C;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,sBAAsB;IACtB,IAAI,EAAE,IAAI,CAAC;IACX,oEAAoE;IACpE,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/B,gCAAgC;IAChC,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,sCAAsC;IACtC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,iBAAiB;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IACjC,mEAAmE;IACnE,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,mFAAmF;IACnF,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,6EAA6E;IAC7E,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,gBAAgB;IAC7D,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,iBAAiB,CAAqB;IAC9C,OAAO,CAAC,cAAc,CAAkB;IACxC,OAAO,CAAC,oBAAoB,CAAwB;IACpD,OAAO,CAAC,kBAAkB,CAAsB;IAChD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,UAAU,CAAU;gBAEhB,OAAO,EAAE,qBAAqB;IAM1C;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAOjC;;;;;;;OAOG;IACG,aAAa,CACjB,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,GACtC,OAAO,CAAC,MAAM,CAAC;IAalB;;;;OAIG;IACG,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IA6C3E;;OAEG;IACH,WAAW;IAIX;;OAEG;IACG,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIzD;;OAEG;IACG,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIzD;;OAEG;IACG,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI7D;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACG,YAAY,CAChB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,OAAO,CAAC,kBAAkB,CAAC;IAyD9B;;OAEG;IACG,eAAe,CAAC,MAAM,EAAE,MAAM;IAIpC;;OAEG;IACG,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC;IAI/C;;OAEG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAM5E;;OAEG;IACG,cAAc,CAAC,CAAC,EACpB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAIzB;;OAEG;IACG,cAAc,CAClB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,OAAO,GACb,OAAO,CAAC,OAAO,CAAC;IAInB;;OAEG;WACU,MAAM,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC;CAK7E"}
1
+ {"version":3,"file":"SessionService.d.ts","sourceRoot":"","sources":["../../src/services/SessionService.ts"],"names":[],"mappings":"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"}
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-07-05T21:23:32.756Z",
3
+ "generatedAt": "2026-07-06T18:42:55.360Z",
4
4
  "packageName": "@happyvertical/smrt-users",
5
- "packageVersion": "0.38.7",
5
+ "packageVersion": "0.38.9",
6
6
  "sourceManifestPath": "dist/manifest.json",
7
7
  "agentDocPath": "AGENTS.md",
8
8
  "sourceHashes": {
9
- "manifest": "80f302de9d7189a825aa37a54495342d7abf4dd025a561429e580d6f3131a1b7",
10
- "packageJson": "6e208cd978125ebd783e204403dc163fbf605ad5717cfeaa0603e8e4180422c5",
11
- "agents": "de9e1d5cf207421ebe0a688f5ca723cfd99d594ad9997bf5e38ac00ab6be8d3c"
9
+ "manifest": "e98a4439c4536eceeb30bf285637afaa344f289f1a7d5c3ab82d4ce324686ab0",
10
+ "packageJson": "500ed3d668ab80c0103a2a7e31ac5e8b435e71dc8cd069372789260b5eb8b259",
11
+ "agents": "60b2f1ee6ab11f5cced88ee7cd0b4361408f2e18c57b71bd27a037bbdd038cd3"
12
12
  },
13
13
  "exports": [
14
14
  ".",
@@ -1368,6 +1368,12 @@
1368
1368
  "type": "boolean",
1369
1369
  "required": false,
1370
1370
  "columnType": "BOOLEAN"
1371
+ },
1372
+ {
1373
+ "name": "inheritsToDescendants",
1374
+ "type": "boolean",
1375
+ "required": false,
1376
+ "columnType": "BOOLEAN"
1371
1377
  }
1372
1378
  ],
1373
1379
  "relationships": [
@@ -2857,5 +2863,5 @@
2857
2863
  "polymorphicAssociations": 0,
2858
2864
  "uuidColumns": 49
2859
2865
  },
2860
- "agentDoc": "# @happyvertical/smrt-users\n\nMulti-tenant user management with RBAC, hierarchical tenants, session handling, and SvelteKit integration.\n\n## Models (14)\n\n| Model | Key Pattern |\n|-------|-------------|\n| User | Auth identity. `profileId` is plain string (not FK) to smrt-profiles. Email auto-lowercased. |\n| AccessRequest | \"Request access / waitlist\" record captured before a `User` exists. CLOSED generated surface (`api`/`mcp`/`cli` = `[]`) — all access via `AccessRequestService`. Email normalized + indexed; JSON `requestContext` (NOT `context` — reserved for slug scoping). |\n| Tenant | **STI** + hierarchical parent-child. `hierarchyPath` (materialized path), `hierarchyLevel`. Max depth 10. |\n| Session | Server-side. Secure UUID. TTL in **seconds** (not ms). Status auto-updates to EXPIRED on access. |\n| MagicLinkToken | Single-use email login token. Backed by `MagicLinkService`. |\n| Role | `tenantId = null` → system role (available to all tenants). `isSystem: true` blocks deletion. |\n| Permission | Slug format: `resource.action`. Parsed by PermissionResolver. |\n| Membership | User + Tenant + Role junction. UNIQUE(userId, tenantId). |\n| Group | Team within a tenant. Multiple roles via GroupRole. |\n| GroupMember, GroupRole, RolePermission | Join tables. |\n| MembershipOverride | Per-user permission grant/deny. **DENY always wins.** |\n| TenantPermissionOverride | Tenant-level cascade overrides. Effect: INHERIT/GRANT/DENY. |\n\n## Permission Resolution — Precedence (broad → specific, most-specific wins)\n\n`PermissionResolver.resolvePermissions` builds the effective set in this order;\neach later layer overrides earlier ones:\n\n1. **Tenant-inherited** — walk ancestors, apply each `TenantPermissionOverride`\n down the cascade (GRANT adds, DENY removes within the hierarchy)\n2. **Membership role** — base permissions from the user's role in the tenant\n3. **Group roles** — permissions from all groups the user belongs to **in that tenant**\n4. **Tenant-level DENY** *(removes; overrides role/group grants, tenant-wide)* — a\n `TenantPermissionOverride` with effect `DENY` is a HARD, tenant-wide block: it\n subtracts the DENY'd slug even if a role or group granted it (steps 2–3). It\n sits just **above** the per-user membership overrides and **below** role/group.\n5. **Membership GRANT override** *(re-adds; most specific)* — a per-user GRANT can\n re-add a slug a tenant DENY'd in step 4, because it is more specific.\n6. **Membership DENY override** *(absolute; always wins)* — a per-user DENY removes\n the slug last and is never overridden.\n\nSo a permission a role grants but the tenant DENYs is **removed**, unless that\nexact user also has a membership-GRANT override for it. A membership-DENY always\nwins. Tenant-DENY of an inherited/cascade grant still blocks it (unchanged).\nThe hard block reflects the tenant cascade's **net** resolution, not an\nunconditional union of every DENY in the chain — so a more-specific tenant GRANT\n(e.g. a child sub-tenant re-granting a permission its parent DENYs) still wins.\n\n## Operation Permission Guards\n\n- Use `assertOperationPermission()` for hand-written mutations in SvelteKit form\n actions, custom endpoints, CLI scripts, and jobs. It derives the same\n `<collection>.<action>` slugs as `PermissionCatalogService` (`list`/`get` →\n `read`), requires the slug to exist in the catalog, then resolves permissions\n through `PermissionResolver`.\n- `assertOperationPermission()` throws fail-closed by default. Use\n `{ onDeny: 'return' }`, `checkOperationPermission()`, or\n `hasOperationPermission()` when a structured/boolean result is needed.\n- System context and super-admin bypass context are honored for parity with\n Postgres RLS. Pass `{ allowSuperAdminBypass: false }` on money-class or\n separation-of-duties operations that must require an explicit permission grant.\n- Seed role mappings with `RolePermissionCollection.seedRolePermissions()` or\n `RoleCollection.seedSystemRoles({ seedPermissions: true })`. The default\n matrix maps owner/admin to all catalog permissions, member to read/create for\n ordinary app resources, and viewer to read-only. Member create grants\n intentionally exclude users/RBAC authority and security resources (`users`,\n `tenants`, `roles`, `permissions`, memberships, groups, sessions, magic-link\n tokens, and related join/override tables). Re-seeding is additive and\n idempotent; pruning stale mappings requires `{ prune: true }`.\n\n**Critical**: `getGroupIdsForTenant(userId, tenantId)` (joins with groups table to scope by tenant). Never use `getGroupIds()` — it's cross-tenant.\n\n## Hierarchical Tenants\n\n- `TenantCollection.createChild()` auto-calculates hierarchy fields, enforces depth limit\n- `moveToParent()` updates tenant + ALL descendants' paths/levels\n- `cascadePermissions` (parent pushes down) + `inheritPermissions` (child accepts) — both must be true\n- `getTree(rootId?)` returns nested structure for UI\n\n## SvelteKit Integration\n\n```typescript\n// hooks.server.ts\nexport const handle = createSessionHandler({ db, ttl: 604800, skipPaths: ['/api/public'] });\n// Populates event.locals: { user, membership, permissions: string[], tenantId, sessionId }\n\n// +page.server.ts\nawait createSessionCookie(event, userId, tenantId, { db });\nawait destroySessionCookie(event, { db });\nawait switchSessionTenant(event, tenantId, { db });\n```\n\n## Security (S5 #1400)\n\n- **Generated REST/MCP surface is READ-ONLY for every RBAC/identity model.**\n User, Tenant, Group, Membership, MembershipOverride, Role, Permission,\n RolePermission, GroupRole, GroupMember, and TenantPermissionOverride generate\n `list`/`get` only — `create`/`update`/`delete` are intentionally NOT\n generated. The merged `requireRouteAuth` gate (#1540) enforces *authentication*,\n not *authorization*, and these models are not `@TenantScoped`, so an\n auto-generated mutating route would let any authenticated user self-grant a\n role/permission, flip a tenant's cascade flags, or change another user's auth\n identity. Mutate them through the permission-gated services (`TenantService`,\n collection helpers) or consumer-owned, permission-checked handlers. A\n structural regression test (`security-audit-1400.test.ts`) enumerates the\n registry to assert no authority model exposes a mutating op. (`cli` stays\n enabled — local-operator surface, outside the network/agent threat model.)\n- **`switchTenant` is fail-closed AND rotates the session id.**\n `SessionService.switchTenant` / `switchSessionTenant` verify the session's user\n has an ACTIVE membership in the target tenant before any write (the tenant id\n is the isolation key for every `@TenantScoped` query). A non-member/unknown-\n session switch returns `{ switched: false, sessionId: null, ... }` and mutates\n nothing. On a successful switch into a NON-null tenant the session id is\n ROTATED: a fresh `Session` (new secure id, fresh TTL, same user, new tenant,\n device context carried over) is minted and the old session is REVOKED — so a\n captured pre-switch id immediately stops validating, shrinking the blast radius\n of a leaked id across a tenant boundary. `switchTenant` returns a\n `SwitchTenantResult` (`{ switched, sessionId, session, rotated }`); callers MUST\n persist the returned `sessionId`. `switchSessionTenant` does this for you by\n re-setting the session cookie (preserving httpOnly/secure/sameSite) to the new\n id. A `null` clear stays in place (no rotation, no cookie change). The\n low-level `SessionCollection.setSessionTenant` is the UNGUARDED primitive (used\n for the null-clear path) — never call it with an untrusted tenant id.\n- **OIDC `email_verified` is enforced.** `UserCollection.getOrCreateFromOidc`\n refuses to provision a user when the IdP explicitly returns\n `email_verified: false` (opt out with `{ allowUnverifiedEmail: true }`). An\n absent claim makes no assertion and is not enforced.\n\n## Gotchas\n\n- **seedSystemRoles() required**: call `RoleCollection.seedSystemRoles()` at app init (creates owner/admin/member/viewer)\n- **PermissionResolver casts `as any`**: collections have protected constructors — known framework limitation\n- **Session TTL in seconds**: `DEFAULT_SESSION_TTL = 7 * 24 * 60 * 60` (not milliseconds)\n- **Users are cross-tenant**: one user, many tenants via Membership. Email globally unique.\n- **Batch permission queries**: resolver fetches all permission IDs in one query, then maps to slugs (avoids N+1)\n"
2866
+ "agentDoc": "# @happyvertical/smrt-users\n\nMulti-tenant user management with RBAC, hierarchical tenants, session handling, and SvelteKit integration.\n\n## Models (14)\n\n| Model | Key Pattern |\n|-------|-------------|\n| User | Auth identity. `profileId` is plain string (not FK) to smrt-profiles. Email auto-lowercased. |\n| AccessRequest | \"Request access / waitlist\" record captured before a `User` exists. CLOSED generated surface (`api`/`mcp`/`cli` = `[]`) — all access via `AccessRequestService`. Email normalized + indexed; JSON `requestContext` (NOT `context` — reserved for slug scoping). |\n| Tenant | **STI** + hierarchical parent-child. `hierarchyPath` (materialized path), `hierarchyLevel`. Max depth 10. |\n| Session | Server-side. Secure UUID. TTL in **seconds** (not ms). Status auto-updates to EXPIRED on access. |\n| MagicLinkToken | Single-use email login token. Backed by `MagicLinkService`. |\n| Role | `tenantId = null` → system role (available to all tenants). `isSystem: true` blocks deletion. `inheritsToDescendants: true` (default false) opts the role's membership authority into descendant tenants. |\n| Permission | Slug format: `resource.action`. Parsed by PermissionResolver. |\n| Membership | User + Tenant + Role junction. UNIQUE(userId, tenantId). |\n| Group | Team within a tenant. Multiple roles via GroupRole. |\n| GroupMember, GroupRole, RolePermission | Join tables. |\n| MembershipOverride | Per-user permission grant/deny. **DENY always wins.** |\n| TenantPermissionOverride | Tenant-level cascade overrides. Effect: INHERIT/GRANT/DENY. |\n\n## Permission Resolution — Precedence (broad → specific, most-specific wins)\n\n`PermissionResolver.resolvePermissions` builds the effective set in this order;\neach later layer overrides earlier ones:\n\n1. **Tenant-inherited** — walk ancestors, apply each `TenantPermissionOverride`\n down the cascade (GRANT adds, DENY removes within the hierarchy)\n2. **Membership role** — base permissions from the user's role in the tenant\n3. **Group roles** — permissions from all groups the user belongs to **in that tenant**\n4. **Tenant-level DENY** *(removes; overrides role/group grants, tenant-wide)* — a\n `TenantPermissionOverride` with effect `DENY` is a HARD, tenant-wide block: it\n subtracts the DENY'd slug even if a role or group granted it (steps 2–3). It\n sits just **above** the per-user membership overrides and **below** role/group.\n5. **Membership GRANT override** *(re-adds; most specific)* — a per-user GRANT can\n re-add a slug a tenant DENY'd in step 4, because it is more specific.\n6. **Membership DENY override** *(absolute; always wins)* — a per-user DENY removes\n the slug last and is never overridden.\n\nSo a permission a role grants but the tenant DENYs is **removed**, unless that\nexact user also has a membership-GRANT override for it. A membership-DENY always\nwins. Tenant-DENY of an inherited/cascade grant still blocks it (unchanged).\nThe hard block reflects the tenant cascade's **net** resolution, not an\nunconditional union of every DENY in the chain — so a more-specific tenant GRANT\n(e.g. a child sub-tenant re-granting a permission its parent DENYs) still wins.\n\n### Membership selection — hierarchical inheritance (opt-in, #1866)\n\nWhich membership feeds step 2 above:\n\n1. **A direct membership row in the target tenant always pins resolution to\n itself** — active rows resolve normally; an inactive (pending/suspended)\n row resolves to the **empty set**. A direct row therefore *attenuates*\n rather than unions: \"viewer here, despite being network admin\" gives\n viewer, and a suspension in the child is effective even for users holding\n inheritable authority on an ancestor.\n2. **No direct row** → the resolver walks the tenant's ancestors (nearest\n first, from `hierarchyPath`) and resolves through the **nearest ACTIVE\n ancestor membership whose role has `inheritsToDescendants: true`**.\n Unflagged or inactive ancestor memberships are skipped (they neither confer\n nor block); there is **no union across the chain** — the nearest flagged\n membership alone is used. To attenuate a specific descendant, create a\n direct membership there or use a tenant-level DENY.\n3. **No qualifying ancestor** → empty set (byte-identical to the\n pre-inheritance resolver; with no role flagged, nothing changes).\n\nAll later layers run unchanged against the **target** tenant: the tenant\ncascade and tenant-DENY hard block come from the target tenant (a child can\ncarve authority out of an inherited role), group roles stay exact-tenant (only\ntarget-tenant groups contribute; ancestor groups never flow down), and\nmembership GRANT/DENY overrides travel with the ancestor membership used.\n`PermissionResolutionResult.inheritedFromTenantId` reports the ancestor tenant\nwhen inheritance was used (`null` for direct resolution).\n\nSafety: resolution is bounded by `MAX_TENANT_HIERARCHY_DEPTH`; malformed\n`hierarchyPath` values fail closed to the empty set — too deep,\nself-referential, duplicate ancestors, or inconsistent with the actual\n`parentTenantId` chain (the path is verified link-by-link against the loaded\nancestor rows before it is trusted as an authorization source). Tenant `status` is not consulted (parity with direct\nresolution). Caching: a long-lived `(user, tenant)` permission cache must also\ninvalidate on ancestor-membership changes and on `Role.inheritsToDescendants`\nflips — request-scoped caches (the common pattern) are unaffected.\n\nFlag roles at seed time with\n`seedSystemRoles({ inheritsToDescendants: ['owner', 'admin'] })` (additive:\nlisted slugs are flagged, omitted slugs are never unflagged; unknown slugs\nthrow). The default seed leaves every role exact-tenant.\n\n## Operation Permission Guards\n\n- Use `assertOperationPermission()` for hand-written mutations in SvelteKit form\n actions, custom endpoints, CLI scripts, and jobs. It derives the same\n `<collection>.<action>` slugs as `PermissionCatalogService` (`list`/`get` →\n `read`), requires the slug to exist in the catalog, then resolves permissions\n through `PermissionResolver`.\n- `assertOperationPermission()` throws fail-closed by default. Use\n `{ onDeny: 'return' }`, `checkOperationPermission()`, or\n `hasOperationPermission()` when a structured/boolean result is needed.\n- **Resource-tenant calling convention**: for resource-anchored authorization,\n pass the **resource's** tenant id — `tenantId: resourceTenantId` — not the\n session's current tenant. With `Role.inheritsToDescendants` flagged, a\n root-tenant admin then passes for any descendant resource with no app-side\n authority logic (and no membership fan-out), while per-child delegation and\n DENY precedence keep working. Do NOT pass a session-scoped `membership`\n alongside a different resource `tenantId` — a membership/tenant mismatch\n fails closed by design; omit `membership` and let the resolver look it up.\n- System context and super-admin bypass context are honored for parity with\n Postgres RLS. Pass `{ allowSuperAdminBypass: false }` on money-class or\n separation-of-duties operations that must require an explicit permission grant.\n- **Postgres RLS and membership inheritance**: RLS policies check the\n session-injected `smrt.permissions` list (resolved app-side by\n `PermissionResolver`), so a session whose `smrt.tenant_id` IS the child\n tenant gets inherited authority in RLS automatically. But RLS row filtering\n stays bound to the session's tenant setting — a root-tenant session acting\n on child-tenant rows is authorized by the app-level guard (resource-tenant\n convention above), not by RLS. This is a documented divergence, mirroring\n how #1829 handled bypass parity.\n- Seed role mappings with `RolePermissionCollection.seedRolePermissions()` or\n `RoleCollection.seedSystemRoles({ seedPermissions: true })`. The default\n matrix maps owner/admin to all catalog permissions, member to read/create for\n ordinary app resources, and viewer to read-only. Member create grants\n intentionally exclude users/RBAC authority and security resources (`users`,\n `tenants`, `roles`, `permissions`, memberships, groups, sessions, magic-link\n tokens, and related join/override tables). Re-seeding is additive and\n idempotent; pruning stale mappings requires `{ prune: true }`.\n\n**Critical**: `getGroupIdsForTenant(userId, tenantId)` (joins with groups table to scope by tenant). Never use `getGroupIds()` — it's cross-tenant.\n\n## Hierarchical Tenants\n\n- `TenantCollection.createChild()` auto-calculates hierarchy fields, enforces depth limit\n- `moveToParent()` updates tenant + ALL descendants' paths/levels\n- `cascadePermissions` (parent pushes down) + `inheritPermissions` (child accepts) — both must be true\n- `getTree(rootId?)` returns nested structure for UI\n- Two independent downward flows: the `TenantPermissionOverride` **cascade**\n (tenant-level permission config, flags above) and **membership-role\n inheritance** (`Role.inheritsToDescendants`, per-role opt-in — see\n \"Membership selection\" above). The cascade flags do not gate membership\n inheritance.\n\n## SvelteKit Integration\n\n```typescript\n// hooks.server.ts\nexport const handle = createSessionHandler({ db, ttl: 604800, skipPaths: ['/api/public'] });\n// Populates event.locals: { user, membership, permissions: string[], tenantId, sessionId }\n\n// +page.server.ts\nawait createSessionCookie(event, userId, tenantId, { db });\nawait destroySessionCookie(event, { db });\nawait switchSessionTenant(event, tenantId, { db });\n```\n\n## Security (S5 #1400)\n\n- **Generated REST/MCP surface is READ-ONLY for every RBAC/identity model.**\n User, Tenant, Group, Membership, MembershipOverride, Role, Permission,\n RolePermission, GroupRole, GroupMember, and TenantPermissionOverride generate\n `list`/`get` only — `create`/`update`/`delete` are intentionally NOT\n generated. The merged `requireRouteAuth` gate (#1540) enforces *authentication*,\n not *authorization*, and these models are not `@TenantScoped`, so an\n auto-generated mutating route would let any authenticated user self-grant a\n role/permission, flip a tenant's cascade flags, or change another user's auth\n identity. Mutate them through the permission-gated services (`TenantService`,\n collection helpers) or consumer-owned, permission-checked handlers. A\n structural regression test (`security-audit-1400.test.ts`) enumerates the\n registry to assert no authority model exposes a mutating op. (`cli` stays\n enabled — local-operator surface, outside the network/agent threat model.)\n- **`switchTenant` is fail-closed AND rotates the session id.**\n `SessionService.switchTenant` / `switchSessionTenant` verify the session's user\n has an ACTIVE membership in the target tenant before any write (the tenant id\n is the isolation key for every `@TenantScoped` query). A non-member/unknown-\n session switch returns `{ switched: false, sessionId: null, ... }` and mutates\n nothing. On a successful switch into a NON-null tenant the session id is\n ROTATED: a fresh `Session` (new secure id, fresh TTL, same user, new tenant,\n device context carried over) is minted and the old session is REVOKED — so a\n captured pre-switch id immediately stops validating, shrinking the blast radius\n of a leaked id across a tenant boundary. `switchTenant` returns a\n `SwitchTenantResult` (`{ switched, sessionId, session, rotated }`); callers MUST\n persist the returned `sessionId`. `switchSessionTenant` does this for you by\n re-setting the session cookie (preserving httpOnly/secure/sameSite) to the new\n id. A `null` clear stays in place (no rotation, no cookie change). The\n low-level `SessionCollection.setSessionTenant` is the UNGUARDED primitive (used\n for the null-clear path) — never call it with an untrusted tenant id.\n- **OIDC `email_verified` is enforced.** `UserCollection.getOrCreateFromOidc`\n refuses to provision a user when the IdP explicitly returns\n `email_verified: false` (opt out with `{ allowUnverifiedEmail: true }`). An\n absent claim makes no assertion and is not enforced.\n\n## Gotchas\n\n- **seedSystemRoles() required**: call `RoleCollection.seedSystemRoles()` at app init (creates owner/admin/member/viewer)\n- **PermissionResolver casts `as any`**: collections have protected constructors — known framework limitation\n- **Session TTL in seconds**: `DEFAULT_SESSION_TTL = 7 * 24 * 60 * 60` (not milliseconds)\n- **Users are cross-tenant**: one user, many tenants via Membership. Email globally unique.\n- **Batch permission queries**: resolver fetches all permission IDs in one query, then maps to slugs (avoids N+1)\n"
2861
2867
  }
package/dist/sveltekit.js CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as resolveOidcProviderConfig, a as TerminalAuthRateLimitError, f as OidcLoginError, g as getUsersOidcConfig, h as encodeOidcTransaction, i as TerminalAuthError, l as withSessionPermissionContext, m as decodeOidcTransaction, o as TerminalAuthService, p as OidcLoginService, u as SessionService } from "./chunks/TerminalAuthService-PONT33zt.js";
1
+ import { _ as resolveOidcProviderConfig, a as TerminalAuthRateLimitError, f as OidcLoginError, g as getUsersOidcConfig, h as encodeOidcTransaction, i as TerminalAuthError, l as withSessionPermissionContext, m as decodeOidcTransaction, o as TerminalAuthService, p as OidcLoginService, u as SessionService } from "./chunks/TerminalAuthService-BgwNOEPM.js";
2
2
  import { ObjectRegistry } from "@happyvertical/smrt-core";
3
3
  import { createLogger } from "@happyvertical/logger";
4
4
  import { classnameToTablename } from "@happyvertical/smrt-core/utils";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-users",
3
- "version": "0.38.7",
3
+ "version": "0.38.9",
4
4
  "description": "Multi-tenant user management for the SMRT framework - users, tenants, roles, permissions, groups",
5
5
  "type": "module",
6
6
  "smrtRawPrimitives": "strict",
@@ -43,12 +43,12 @@
43
43
  "dependencies": {
44
44
  "@happyvertical/logger": "^0.76.2",
45
45
  "jose": "^6.2.3",
46
- "@happyvertical/smrt-config": "0.38.7",
47
- "@happyvertical/smrt-core": "0.38.7",
48
- "@happyvertical/smrt-profiles": "0.38.7",
49
- "@happyvertical/smrt-tenancy": "0.38.7",
50
- "@happyvertical/smrt-types": "0.38.7",
51
- "@happyvertical/smrt-ui": "0.38.7"
46
+ "@happyvertical/smrt-core": "0.38.9",
47
+ "@happyvertical/smrt-config": "0.38.9",
48
+ "@happyvertical/smrt-tenancy": "0.38.9",
49
+ "@happyvertical/smrt-types": "0.38.9",
50
+ "@happyvertical/smrt-profiles": "0.38.9",
51
+ "@happyvertical/smrt-ui": "0.38.9"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@happyvertical/sql": "^0.76.2",
@@ -61,7 +61,7 @@
61
61
  "typescript": "^5.9.3",
62
62
  "vite": "^8.1.3",
63
63
  "vitest": "^4.1.9",
64
- "@happyvertical/smrt-vitest": "0.38.7"
64
+ "@happyvertical/smrt-vitest": "0.38.9"
65
65
  },
66
66
  "keywords": [
67
67
  "smrt",