@happyvertical/smrt-users 0.51.14 → 0.51.16

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.
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-users",
5
- "packageVersion": "0.51.14",
5
+ "packageVersion": "0.51.16",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-users:AccessRequestCollection": {
8
8
  "name": "accessrequestcollection",
@@ -3122,20 +3122,6 @@
3122
3122
  "returnType": "any",
3123
3123
  "isStatic": false,
3124
3124
  "isPublic": true
3125
- },
3126
- "create": {
3127
- "name": "create",
3128
- "async": true,
3129
- "parameters": [
3130
- {
3131
- "name": "options",
3132
- "type": "SmrtCreateInput<Tenant>",
3133
- "optional": false
3134
- }
3135
- ],
3136
- "returnType": "Promise<Tenant>",
3137
- "isStatic": false,
3138
- "isPublic": true
3139
3125
  }
3140
3126
  },
3141
3127
  "decoratorConfig": {},
@@ -6798,6 +6784,20 @@
6798
6784
  "isStatic": false,
6799
6785
  "isPublic": true
6800
6786
  },
6787
+ "save": {
6788
+ "name": "save",
6789
+ "async": true,
6790
+ "parameters": [
6791
+ {
6792
+ "name": "options",
6793
+ "type": "SmrtSaveOptions",
6794
+ "optional": true
6795
+ }
6796
+ ],
6797
+ "returnType": "Promise<this>",
6798
+ "isStatic": false,
6799
+ "isPublic": true
6800
+ },
6801
6801
  "getAncestorIds": {
6802
6802
  "name": "getAncestorIds",
6803
6803
  "async": false,
@@ -0,0 +1,52 @@
1
+ import { getDatabase } from '@happyvertical/sql';
2
+ import { TenantHierarchyPlan, TenantHierarchyProblem } from '../models/tenant-hierarchy.js';
3
+ type DatabaseInterface = Awaited<ReturnType<typeof getDatabase>>;
4
+ /** Options for {@link materializeTenantHierarchy}. */
5
+ export interface MaterializeTenantHierarchyOptions {
6
+ /** Report what would change without writing. Default `false`. */
7
+ dryRun?: boolean;
8
+ /** The Tenant STI table. Default `'tenants'`. */
9
+ tableName?: string;
10
+ }
11
+ /** Result of {@link materializeTenantHierarchy}. */
12
+ export interface MaterializeTenantHierarchyResult extends TenantHierarchyPlan {
13
+ /** `true` when the planned changes were written. */
14
+ applied: boolean;
15
+ }
16
+ /**
17
+ * Raised before any row is changed when a tenant's parent chain is broken
18
+ * (missing parent, cycle, or too deep), so an operator can repair
19
+ * `parent_tenant_id` explicitly instead of the backfill guessing at an
20
+ * authorization source.
21
+ */
22
+ export declare class TenantHierarchyMaterializationError extends Error {
23
+ readonly problems: TenantHierarchyProblem[];
24
+ constructor(problems: TenantHierarchyProblem[]);
25
+ }
26
+ /**
27
+ * Backfill `hierarchy_path` / `hierarchy_level` for every tenant from its
28
+ * `parent_tenant_id` chain (smrt#3036).
29
+ *
30
+ * `Tenant.save()` keeps these current from now on; this repairs rows written
31
+ * before the framework maintained them — typically every row created through
32
+ * a path other than `TenantCollection.createChild()`, which left an empty path
33
+ * and level 0 even with a correct parent. Until repaired, both
34
+ * `inheritsToDescendants` and the declared ancestor-read policy fail closed on
35
+ * those tenants.
36
+ *
37
+ * Idempotent: it writes only rows that differ, so a second run reports zero
38
+ * changes. It refuses (writing nothing) when any tenant's chain is broken;
39
+ * `dryRun` reports the same plan, problems included, without throwing.
40
+ * Runs in one transaction and refuses an adapter without `transaction()`
41
+ * (dry runs excepted). Touches only the two
42
+ * derived columns (not `updated_at`) and records one table-level change-feed
43
+ * entry after commit.
44
+ *
45
+ * Exposed as `smrt db:materialize-tenant-hierarchy [--dry-run]`.
46
+ *
47
+ * @throws {TenantHierarchyMaterializationError} when not a dry run and any
48
+ * tenant's parent chain is broken.
49
+ */
50
+ export declare function materializeTenantHierarchy(db: DatabaseInterface, options?: MaterializeTenantHierarchyOptions): Promise<MaterializeTenantHierarchyResult>;
51
+ export {};
52
+ //# sourceMappingURL=materializeTenantHierarchy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"materializeTenantHierarchy.d.ts","sourceRoot":"","sources":["../../src/migrations/materializeTenantHierarchy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAML,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAE5B,MAAM,+BAA+B,CAAC;AAEvC,KAAK,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC;AAEjE,sDAAsD;AACtD,MAAM,WAAW,iCAAiC;IAChD,iEAAiE;IACjE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,oDAAoD;AACpD,MAAM,WAAW,gCAAiC,SAAQ,mBAAmB;IAC3E,oDAAoD;IACpD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,qBAAa,mCAAoC,SAAQ,KAAK;IAChD,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,EAAE;gBAAlC,QAAQ,EAAE,sBAAsB,EAAE;CAOxD;AAoCD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,iBAAiB,EACrB,OAAO,GAAE,iCAAsC,GAC9C,OAAO,CAAC,gCAAgC,CAAC,CAiC3C"}
@@ -1,4 +1,4 @@
1
- import { SmrtObject, SmrtObjectOptions } from '@happyvertical/smrt-core';
1
+ import { SmrtObject, SmrtObjectOptions, SmrtSaveOptions } from '@happyvertical/smrt-core';
2
2
  import { Tenant as TenantContract } from '@happyvertical/smrt-types';
3
3
  import { TenantStatus } from '../types/index.js';
4
4
  /**
@@ -14,11 +14,7 @@ export interface TenantOptions extends SmrtObjectOptions {
14
14
  cascadePermissions?: boolean;
15
15
  inheritPermissions?: boolean;
16
16
  }
17
- /**
18
- * Maximum allowed depth for tenant hierarchy.
19
- * Prevents excessively deep trees that could cause performance issues.
20
- */
21
- export declare const MAX_TENANT_HIERARCHY_DEPTH = 10;
17
+ export { MAX_TENANT_HIERARCHY_DEPTH } from './tenant-hierarchy.js';
22
18
  /**
23
19
  * Tenant represents an organizational boundary in the multi-tenant system.
24
20
  *
@@ -88,14 +84,18 @@ export declare class Tenant extends SmrtObject implements TenantContract {
88
84
  parentTenantId?: string | null;
89
85
  /**
90
86
  * Depth in the hierarchy tree (0 = root, 1 = first level child, etc.)
91
- * Automatically managed by TenantCollection methods.
87
+ * Derived from `parentTenantId` and maintained by {@link Tenant.save};
88
+ * any value assigned directly is recomputed on save.
92
89
  */
93
90
  hierarchyLevel: number;
94
91
  /**
95
92
  * Materialized path for efficient tree traversal.
96
93
  * Format: "ancestor-id/parent-id" (path to parent; does not include this tenant's id)
97
94
  * Empty string for root tenants.
98
- * Automatically managed by TenantCollection methods.
95
+ * Derived from `parentTenantId` and maintained by {@link Tenant.save};
96
+ * any value assigned directly is recomputed on save. Rows written before
97
+ * the framework maintained it are backfilled with
98
+ * `smrt db:materialize-tenant-hierarchy` (smrt#3036).
99
99
  */
100
100
  hierarchyPath: string;
101
101
  /**
@@ -141,6 +141,38 @@ export declare class Tenant extends SmrtObject implements TenantContract {
141
141
  * for accurate permission calculation.
142
142
  */
143
143
  acceptsInheritance(): boolean;
144
+ /**
145
+ * Persist the tenant, keeping the derived hierarchy fields true (smrt#3036).
146
+ *
147
+ * `hierarchyPath` / `hierarchyLevel` are recomputed from the real
148
+ * `parentTenantId` chain on EVERY save — whichever collection, subclass, or
149
+ * code path produced the object — so a consumer never hand-maintains them.
150
+ * When they change (a reparent, or healing a stale row), every descendant is
151
+ * re-materialized too. Both columns are an authorization source for
152
+ * `inheritsToDescendants` and the declared ancestor-read policy.
153
+ *
154
+ * @throws {TenantHierarchyError} before anything is written, when the new
155
+ * parent is missing, would create a cycle, or would put this tenant or one
156
+ * of its descendants at or below `MAX_TENANT_HIERARCHY_DEPTH`.
157
+ */
158
+ save(options?: SmrtSaveOptions): Promise<this>;
159
+ /**
160
+ * Refuse to link this tenant under a parent the caller cannot see.
161
+ *
162
+ * A parent link is authority-bearing: ancestor tenant overrides cascade down
163
+ * it and `inheritsToDescendants` roles flow along it. The chain itself is
164
+ * resolved with raw reads (so paths can be derived for any row), so this is
165
+ * the check that keeps a NEW or CHANGED link subject to the caller's own
166
+ * tenancy scope, whichever path made it — `createChild`, `moveToParent`,
167
+ * `create({ parentTenantId })`, or a plain `parentTenantId` assignment and
168
+ * `save()`. The parent is loaded through `TenantCollection`, i.e. through
169
+ * the tenancy interceptor: a refused or filtered read fails the save before
170
+ * anything is written. System context (migrations, backfills, platform
171
+ * administration) bypasses the interceptor as everywhere else. An unchanged
172
+ * link is not re-checked, so a tenant can still save itself under its own
173
+ * scope when its parent is not visible there.
174
+ */
175
+ private assertNewParentVisible;
144
176
  /**
145
177
  * Get ancestor IDs from the hierarchy path.
146
178
  * Returns an array of tenant IDs from root to immediate parent.
@@ -1 +1 @@
1
- {"version":3,"file":"Tenant.d.ts","sourceRoot":"","sources":["../../src/models/Tenant.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAGL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,IAAI,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;GAGG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,qBASa,MAAO,SAAQ,UAAW,YAAW,cAAc;IAC9D;;OAEG;IACH,IAAI,EAAE,MAAM,CAAM;IAElB;;OAEG;IAEH,MAAM,EAAE,YAAY,CAAuB;IAE3C;;OAEG;IACH,WAAW,EAAE,MAAM,CAAM;IAIzB;;;OAGG;IAEH,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAE/B;;;OAGG;IACH,cAAc,EAAE,MAAM,CAAK;IAE3B;;;;;OAKG;IACH,aAAa,EAAE,MAAM,CAAM;IAI3B;;;;OAIG;IACH,kBAAkB,EAAE,OAAO,CAAQ;IAEnC;;;;OAIG;IACH,kBAAkB,EAAE,OAAO,CAAQ;gBAEvB,OAAO,GAAE,aAAkB;IAkBvC;;OAEG;IACH,QAAQ,IAAI,OAAO;IAInB;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,MAAM,IAAI,OAAO;IAIjB;;;;;OAKG;IACH,oBAAoB,IAAI,OAAO;IAI/B;;;;;;;;;OASG;IACH,kBAAkB,IAAI,OAAO;IAI7B;;;;OAIG;IACH,cAAc,IAAI,MAAM,EAAE;CAI3B"}
1
+ {"version":3,"file":"Tenant.d.ts","sourceRoot":"","sources":["../../src/models/Tenant.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAGL,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,IAAI,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAUjD;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,OAAO,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,qBASa,MAAO,SAAQ,UAAW,YAAW,cAAc;IAC9D;;OAEG;IACH,IAAI,EAAE,MAAM,CAAM;IAElB;;OAEG;IAEH,MAAM,EAAE,YAAY,CAAuB;IAE3C;;OAEG;IACH,WAAW,EAAE,MAAM,CAAM;IAIzB;;;OAGG;IAEH,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAE/B;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAK;IAE3B;;;;;;;;OAQG;IACH,aAAa,EAAE,MAAM,CAAM;IAI3B;;;;OAIG;IACH,kBAAkB,EAAE,OAAO,CAAQ;IAEnC;;;;OAIG;IACH,kBAAkB,EAAE,OAAO,CAAQ;gBAEvB,OAAO,GAAE,aAAkB;IAkBvC;;OAEG;IACH,QAAQ,IAAI,OAAO;IAInB;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,MAAM,IAAI,OAAO;IAIjB;;;;;OAKG;IACH,oBAAoB,IAAI,OAAO;IAI/B;;;;;;;;;OASG;IACH,kBAAkB,IAAI,OAAO;IAI7B;;;;;;;;;;;;;OAaG;IACY,IAAI,CAAC,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAuCjE;;;;;;;;;;;;;;;OAeG;YACW,sBAAsB;IAoCpC;;;;OAIG;IACH,cAAc,IAAI,MAAM,EAAE;CAI3B"}
@@ -0,0 +1,105 @@
1
+ import { bumpChangeFeed } from '@happyvertical/smrt-core';
2
+ /**
3
+ * Maximum allowed depth for tenant hierarchy.
4
+ * Prevents excessively deep trees that could cause performance issues.
5
+ */
6
+ export declare const MAX_TENANT_HIERARCHY_DEPTH = 10;
7
+ /** Reasons a tenant hierarchy cannot be (re)materialized. */
8
+ export type TenantHierarchyErrorCode = 'CIRCULAR_REFERENCE' | 'MAX_DEPTH_EXCEEDED' | 'PARENT_NOT_FOUND' | 'INVALID_OPERATION';
9
+ /**
10
+ * Error thrown when tenant hierarchy operations fail
11
+ */
12
+ export declare class TenantHierarchyError extends Error {
13
+ readonly code: TenantHierarchyErrorCode;
14
+ constructor(message: string, code: TenantHierarchyErrorCode);
15
+ }
16
+ /** The derived hierarchy columns of one tenant row. */
17
+ export interface TenantHierarchyFields {
18
+ hierarchyPath: string;
19
+ hierarchyLevel: number;
20
+ }
21
+ /**
22
+ * The minimal database surface these helpers need — satisfied by every
23
+ * `@happyvertical/sql` adapter and transaction handle.
24
+ */
25
+ export interface TenantHierarchyDatabase {
26
+ query: (sql: string, ...vars: unknown[]) => Promise<{
27
+ rows: Array<Record<string, unknown>>;
28
+ }>;
29
+ }
30
+ /** Quote-free guard: the table name is interpolated, so it must be a plain identifier. */
31
+ export declare function assertTenantTableName(tableName: string): string;
32
+ /** The path a child of `parent` must carry. */
33
+ export declare function childHierarchyFields(parentId: string, parent: TenantHierarchyFields): TenantHierarchyFields;
34
+ /**
35
+ * Compute a tenant's hierarchy fields from the REAL `parentTenantId` chain,
36
+ * never from any ancestor's materialized path (which may itself be stale).
37
+ *
38
+ * @throws {TenantHierarchyError} `PARENT_NOT_FOUND` when a link is missing,
39
+ * `CIRCULAR_REFERENCE` when the chain loops or reaches `tenantId`,
40
+ * `MAX_DEPTH_EXCEEDED` when the tenant would sit at or below
41
+ * {@link MAX_TENANT_HIERARCHY_DEPTH}.
42
+ */
43
+ export declare function computeTenantHierarchyFields(db: TenantHierarchyDatabase, tableName: string, tenantId: string | null | undefined, parentTenantId: string | null | undefined): Promise<TenantHierarchyFields>;
44
+ /** One pending descendant rewrite. */
45
+ export interface TenantHierarchyUpdate extends TenantHierarchyFields {
46
+ id: string;
47
+ }
48
+ /**
49
+ * Plan the descendant rewrites implied by `rootId` now carrying `root`.
50
+ * Read-only; walks `parent_tenant_id` links breadth-first, so it works even
51
+ * when descendants were never materialized.
52
+ *
53
+ * @throws {TenantHierarchyError} `MAX_DEPTH_EXCEEDED` when a descendant would
54
+ * end up too deep, `CIRCULAR_REFERENCE` when the subtree loops.
55
+ */
56
+ export declare function planDescendantHierarchy(db: TenantHierarchyDatabase, tableName: string, rootId: string, root: TenantHierarchyFields): Promise<TenantHierarchyUpdate[]>;
57
+ /** Write planned hierarchy fields. Touches only the two derived columns. */
58
+ export declare function applyTenantHierarchyUpdates(db: TenantHierarchyDatabase, tableName: string, updates: readonly TenantHierarchyUpdate[]): Promise<void>;
59
+ /** A stored tenant row, as the backfill planner sees it. */
60
+ export interface TenantHierarchyRow {
61
+ id: string;
62
+ parentTenantId: string | null;
63
+ hierarchyPath: string | null;
64
+ hierarchyLevel: number | null;
65
+ }
66
+ /** A row the planner refuses to materialize, and why. */
67
+ export interface TenantHierarchyProblem {
68
+ id: string;
69
+ code: Exclude<TenantHierarchyErrorCode, 'INVALID_OPERATION'>;
70
+ message: string;
71
+ }
72
+ /** A single planned change: what is stored, and what it must become. */
73
+ export interface TenantHierarchyChange extends TenantHierarchyUpdate {
74
+ previousPath: string | null;
75
+ previousLevel: number | null;
76
+ }
77
+ /** Result of {@link planTenantHierarchy}. */
78
+ export interface TenantHierarchyPlan {
79
+ /** Rows examined. */
80
+ total: number;
81
+ /** Rows whose stored fields differ from the parent chain. */
82
+ changes: TenantHierarchyChange[];
83
+ /** Rows whose parent chain is broken; nothing is written while any exist. */
84
+ problems: TenantHierarchyProblem[];
85
+ }
86
+ /**
87
+ * Pure planner: compute every tenant's hierarchy fields from the
88
+ * `parentTenantId` graph of `rows` and report what differs. Rows whose chain
89
+ * is broken (missing parent, cycle, too deep) are reported as problems rather
90
+ * than guessed at, because the result is an authorization source.
91
+ */
92
+ export declare function planTenantHierarchy(rows: readonly TenantHierarchyRow[]): TenantHierarchyPlan;
93
+ /**
94
+ * Record raw hierarchy rewrites in the change feed through core's documented
95
+ * out-of-band escape hatch, so feed consumers observe that a row's ancestry
96
+ * changed. `rowIds` omitted records one table-level change.
97
+ *
98
+ * Mirrors the framework writer's failure policy: the write already succeeded
99
+ * and must not be un-succeeded by feed bookkeeping, so failures are logged and
100
+ * swallowed. `updated_at` is deliberately NOT bumped — the two columns are
101
+ * derived, and every `Tenant.save()` recomputes them, so a concurrent holder
102
+ * of a stale instance heals rather than conflicts.
103
+ */
104
+ export declare function recordTenantHierarchyChanges(db: Parameters<typeof bumpChangeFeed>[0], tableName: string, rowIds?: readonly string[]): Promise<void>;
105
+ //# sourceMappingURL=tenant-hierarchy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-hierarchy.d.ts","sourceRoot":"","sources":["../../src/models/tenant-hierarchy.ts"],"names":[],"mappings":"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"}
@@ -196,6 +196,27 @@ export declare class PermissionResolver {
196
196
  * 4. Return the final effective permission set
197
197
  */
198
198
  resolveTenantPermissions(tenantId: string): Promise<TenantPermissionInheritanceResult>;
199
+ private resolveTenantPermissionsInternal;
200
+ /**
201
+ * The tenant's ancestors, root first, proven against real `parentTenantId`
202
+ * links (smrt#3036).
203
+ *
204
+ * The materialized `hierarchyPath` is used when it agrees link-by-link with
205
+ * the parent chain. When it does not — a legacy row with a correct parent
206
+ * and an empty path, or a stale or forged path — the chain is rebuilt by
207
+ * walking `parentTenantId`, so a path can never make an unrelated tenant's
208
+ * overrides cascade in, and a never-materialized row still receives its
209
+ * real ancestors' DENYs. A parent chain that is itself broken (missing
210
+ * parent, cycle, or deeper than `MAX_TENANT_HIERARCHY_DEPTH`) throws
211
+ * {@link TenantHierarchyError}: the resolution fails closed rather than
212
+ * guessing which overrides apply.
213
+ *
214
+ * `unreadableAncestor: 'truncate'` is for callers outside system context
215
+ * (the display chain), where an ancestor the caller cannot read is
216
+ * indistinguishable from a missing one: the chain then ends at the nearest
217
+ * readable ancestor instead of throwing `PARENT_NOT_FOUND`.
218
+ */
219
+ private loadVerifiedAncestorChain;
199
220
  /**
200
221
  * Get the inheritance chain for a tenant (for debugging/display purposes)
201
222
  */
@@ -245,6 +266,7 @@ export declare class PermissionResolver {
245
266
  * 6. Subtract membership DENY overrides (absolute precedence)
246
267
  */
247
268
  resolvePermissions(userId: string, tenantId: string, options?: PermissionResolutionOptions): Promise<PermissionResolutionResult>;
269
+ private resolvePermissionsInternal;
248
270
  /**
249
271
  * The permission slugs a role grants through the role-permission catalog,
250
272
  * excluding every per-tenant, per-group, and per-membership override.
@@ -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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"}
@@ -3,14 +3,14 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-users",
6
- "packageVersion": "0.51.14",
6
+ "packageVersion": "0.51.16",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "34d2639c51bf646327afea4470649ddf41515beed335a9c185039cc6978b69ab",
11
- "packageJson": "908695fd991f1415e6bce60bc449775393195b3d0c4c610b52ec9917e8408a18",
12
- "agents": "0d011d9fe314ca90141f2fa45a97cdbb34dc34d0bb046357dd6d159c686bc41e",
13
- "moduleDoc:agents/permissions.md": "165fd67e4ff87d2e4c810ec8f6d9ef4d1bd7d4acc7e9ee0510020625fe2f5153",
10
+ "manifest": "b6882674446eda5d6f38704b8029ac78a24cd2daadaf0f35a62629c56a103a2e",
11
+ "packageJson": "1cc836cd453f30426095c1d379325bffef78c8c0470f7de232f613d6e119af0f",
12
+ "agents": "d7f1bb57f7bbbc1b750ab87c33592a53622236d17f49f8c04b15c4745b5c2ab2",
13
+ "moduleDoc:agents/permissions.md": "9d061473b4da77aa853a34d7adf1e4cfbaf5984a3ba3f7408df91db75e9e0c73",
14
14
  "moduleDoc:agents/oidc-provisioning.md": "5efa607ff23b9a693391e0c02670324a8cea4114d4968c9718f10a763da919c6",
15
15
  "moduleDoc:agents/mobile-auth.md": "25e85e83f43e3a46383a92fa809f3a003b4545c3026e78dbe1bccbbc35161e18",
16
16
  "moduleDoc:agents/retention.md": "425c0a34ed33969badfef699751bfa670bdf618eda65e079e00815620aabed68"
@@ -2669,7 +2669,6 @@
2669
2669
  "fields": [],
2670
2670
  "relationships": [],
2671
2671
  "methods": [
2672
- "create",
2673
2672
  "createChild",
2674
2673
  "findActive",
2675
2674
  "findBySlug",
@@ -2689,14 +2688,6 @@
2689
2688
  "validateHierarchy"
2690
2689
  ],
2691
2690
  "methodSignatures": [
2692
- {
2693
- "name": "create",
2694
- "async": true,
2695
- "params": [
2696
- "options: SmrtCreateInput<Tenant>"
2697
- ],
2698
- "returns": "Promise<Tenant>"
2699
- },
2700
2691
  {
2701
2692
  "name": "createChild",
2702
2693
  "async": true,
@@ -6169,7 +6160,8 @@
6169
6160
  "getAncestorIds",
6170
6161
  "isActive",
6171
6162
  "isRoot",
6172
- "isSuspended"
6163
+ "isSuspended",
6164
+ "save"
6173
6165
  ],
6174
6166
  "methodSignatures": [
6175
6167
  {
@@ -6195,6 +6187,14 @@
6195
6187
  {
6196
6188
  "name": "isSuspended",
6197
6189
  "returns": "boolean"
6190
+ },
6191
+ {
6192
+ "name": "save",
6193
+ "async": true,
6194
+ "params": [
6195
+ "options?: SmrtSaveOptions"
6196
+ ],
6197
+ "returns": "Promise<this>"
6198
6198
  }
6199
6199
  ],
6200
6200
  "tableStrategy": "sti",
@@ -6336,6 +6336,13 @@
6336
6336
  "code": "not-included",
6337
6337
  "reason": "not listed in api.include",
6338
6338
  "objectName": "@happyvertical/smrt-users:Tenant"
6339
+ },
6340
+ {
6341
+ "kind": "api",
6342
+ "operation": "save",
6343
+ "code": "lifecycle-method",
6344
+ "reason": "`save` is a framework lifecycle method, not a distinct operation",
6345
+ "objectName": "@happyvertical/smrt-users:Tenant"
6339
6346
  }
6340
6347
  ],
6341
6348
  "relationshipFeatures": [
@@ -10548,12 +10555,12 @@
10548
10555
  "polymorphicAssociations": 0,
10549
10556
  "uuidColumns": 90
10550
10557
  },
10551
- "agentDoc": "# @happyvertical/smrt-users\n\nMulti-tenant identity, RBAC, hierarchical tenants, sessions, and SvelteKit auth.\n\n## Modules\n\nRead only the module relevant to the change; protocol and provisioning details\nare not prerequisites for unrelated user-package work.\n\n| Module | Scope | Module doc |\n|---|---|---|\n| `src/services/PermissionResolver.ts` | permission precedence, inherited memberships, guards, RLS, role seeding | [agents/permissions.md](agents/permissions.md) |\n| `src/services/OidcLoginService.ts`, collections and OIDC handlers | identity reconciliation, transaction boundaries, migration readiness | [agents/oidc-provisioning.md](agents/oidc-provisioning.md) |\n| `src/services/MobileAuthService.ts` | mobile handshake, bearer sessions, bootstrap extension boundary | [agents/mobile-auth.md](agents/mobile-auth.md) |\n| `src/retention.ts` | expired session/token/CLI-auth reaping and retention sweep wiring | [agents/retention.md](agents/retention.md) |\n\n## Models and authority\n\n- Users are global; tenant access comes through Membership (unique user/tenant).\n User email is normalized and globally unique through readonly nullable\n `emailKey`. `profileId` is a unique cross-package reference: at most one User\n owns each non-null Profile.\n- Tenant uses STI and a materialized hierarchy, maximum depth 10.\n `createChild()` calculates paths; `moveToParent()` updates all descendants.\n- Role with `tenantId = null` is available to all tenants; `isSystem` prevents\n deletion. `inheritsToDescendants` is opt-in. Seed owner/admin/member/viewer\n through `RoleCollection.seedSystemRoles()` at application initialization.\n- Group roles apply only in their own tenant. Use `getGroupIdsForTenant()`,\n never cross-tenant `getGroupIds()`, for authorization.\n- Upward visibility is opt-in and read-only: `permissions.ancestorRead`\n ({ roles, collections, maxDepth? }) lets a DESCENDANT membership contribute\n declared `<collection>.read` at an ancestor, only when no membership resolved\n there, intersected with BOTH the role's own catalog grants and the\n principal's effective permissions in that descendant, and only for SYSTEM\n roles (a tenant-scoped role sharing a declared slug is ignored). Off by default, never\n write, never lateral, and never row visibility — sibling rows stay scoped by\n tenancy/RLS. Read the permissions module before changing it.\n- Membership DENY always wins. Direct inactive membership blocks inherited\n authority; a direct active membership pins resolution instead of unioning it\n with ancestors. Read the permissions module before changing these rules.\n- AccessRequest has no generated API/MCP/CLI operations; access it through\n `AccessRequestService`. Its JSON field is `requestContext`, not reserved\n slug-scoping `context`.\n\n## Security boundaries\n\n- Generated REST/MCP operations on identity/RBAC models are list/get only.\n Route authentication does not authorize authority mutations, and these models\n are not tenant-scoped. Use permission-gated services or explicitly checked\n consumer handlers. CLI remains a local-operator surface. Preserve the\n registry assertions in `security-audit-1400.test.ts`.\n- Resource guards take the resource tenant, not the session tenant. Omit a\n session membership when targeting a different tenant; mismatches fail closed.\n `loadSessionContext().tenantAuthorization` must be checked by required-tenant\n consumers: null membership can represent inherited authority.\n- Sessions use secure UUIDs; TTL is seconds (default seven days). Access marks\n expired sessions EXPIRED. Magic-link tokens are single use.\n- Tenant switching verifies active membership before writing and rotates the\n session ID for non-null targets, revoking the old session. Persist the returned\n `SwitchTenantResult.sessionId`; `switchSessionTenant()` updates the cookie and\n preserves its security settings. Failed switches mutate nothing; null clears\n do not rotate. `SessionCollection.setSessionTenant()` is unguarded and must\n never receive an untrusted tenant ID.\n- OIDC provisioning is atomic and fail-closed. Preserve exact issuer/subject\n identifiers, claim-source email verification, unique global Person ownership,\n and transaction-bound reconciliation. Read the OIDC module and canonical\n scenario matrix before changing any provisioning path.\n\n## Entry points and validation\n\n`src/sveltekit/` owns `createSessionHandler`, `createSessionCookie`,\n`destroySessionCookie`, and `switchSessionTenant`. Use README integration examples\nrather than copying application setup into these instructions.\n\nFrom the repository root:\n\n```bash\npnpm --filter @happyvertical/smrt-users test\npnpm --filter @happyvertical/smrt-users typecheck\npnpm --filter @happyvertical/smrt-users test:postgres\n```\n\nStart with the relevant test file via `test -- src/__tests__/<file>.test.ts`.\nRun `test:postgres` for RLS, principal context, OIDC, or terminal-auth database\nchanges. `typecheck` includes Svelte accessibility checks; plain `tsc` is\ninsufficient. Follow root knowledge freshness checks before shipping.\n",
10558
+ "agentDoc": "# @happyvertical/smrt-users\n\nMulti-tenant identity, RBAC, hierarchical tenants, sessions, and SvelteKit auth.\n\n## Modules\n\nRead only the module relevant to the change; protocol and provisioning details\nare not prerequisites for unrelated user-package work.\n\n| Module | Scope | Module doc |\n|---|---|---|\n| `src/services/PermissionResolver.ts` | permission precedence, inherited memberships, guards, RLS, role seeding | [agents/permissions.md](agents/permissions.md) |\n| `src/services/OidcLoginService.ts`, collections and OIDC handlers | identity reconciliation, transaction boundaries, migration readiness | [agents/oidc-provisioning.md](agents/oidc-provisioning.md) |\n| `src/services/MobileAuthService.ts` | mobile handshake, bearer sessions, bootstrap extension boundary | [agents/mobile-auth.md](agents/mobile-auth.md) |\n| `src/retention.ts` | expired session/token/CLI-auth reaping and retention sweep wiring | [agents/retention.md](agents/retention.md) |\n\n## Models and authority\n\n- Users are global; tenant access comes through Membership (unique user/tenant).\n User email is normalized and globally unique through readonly nullable\n `emailKey`. `profileId` is a unique cross-package reference: at most one User\n owns each non-null Profile.\n- Tenant uses STI and a materialized hierarchy, maximum depth 10.\n `hierarchyPath`/`hierarchyLevel` are derived from `parentTenantId` by\n `Tenant.save()` on every save (any caller-supplied value is recomputed) and\n descendants are re-materialized when they change; never hand-maintain them.\n Legacy rows are backfilled with `smrt db:materialize-tenant-hierarchy`\n (`materializeTenantHierarchy()`). Tests that need a corrupt path must write it\n with raw SQL (#3036).\n- Role with `tenantId = null` is available to all tenants; `isSystem` prevents\n deletion. `inheritsToDescendants` is opt-in. Seed owner/admin/member/viewer\n through `RoleCollection.seedSystemRoles()` at application initialization.\n- Group roles apply only in their own tenant. Use `getGroupIdsForTenant()`,\n never cross-tenant `getGroupIds()`, for authorization.\n- Upward visibility is opt-in and read-only: `permissions.ancestorRead`\n ({ roles, collections, maxDepth? }) lets a DESCENDANT membership contribute\n declared `<collection>.read` at an ancestor, only when no membership resolved\n there, intersected with BOTH the role's own catalog grants and the\n principal's effective permissions in that descendant, and only for SYSTEM\n roles (a tenant-scoped role sharing a declared slug is ignored). Off by default, never\n write, never lateral, and never row visibility — sibling rows stay scoped by\n tenancy/RLS. Read the permissions module before changing it.\n- Membership DENY always wins. Direct inactive membership blocks inherited\n authority; a direct active membership pins resolution instead of unioning it\n with ancestors. Read the permissions module before changing these rules.\n- AccessRequest has no generated API/MCP/CLI operations; access it through\n `AccessRequestService`. Its JSON field is `requestContext`, not reserved\n slug-scoping `context`.\n\n## Security boundaries\n\n- Generated REST/MCP operations on identity/RBAC models are list/get only.\n Route authentication does not authorize authority mutations, and these models\n are not tenant-scoped. Use permission-gated services or explicitly checked\n consumer handlers. CLI remains a local-operator surface. Preserve the\n registry assertions in `security-audit-1400.test.ts`.\n- Resource guards take the resource tenant, not the session tenant. Omit a\n session membership when targeting a different tenant; mismatches fail closed.\n `loadSessionContext().tenantAuthorization` must be checked by required-tenant\n consumers: null membership can represent inherited authority.\n- Sessions use secure UUIDs; TTL is seconds (default seven days). Access marks\n expired sessions EXPIRED. Magic-link tokens are single use.\n- Tenant switching verifies active membership before writing and rotates the\n session ID for non-null targets, revoking the old session. Persist the returned\n `SwitchTenantResult.sessionId`; `switchSessionTenant()` updates the cookie and\n preserves its security settings. Failed switches mutate nothing; null clears\n do not rotate. `SessionCollection.setSessionTenant()` is unguarded and must\n never receive an untrusted tenant ID.\n- OIDC provisioning is atomic and fail-closed. Preserve exact issuer/subject\n identifiers, claim-source email verification, unique global Person ownership,\n and transaction-bound reconciliation. Read the OIDC module and canonical\n scenario matrix before changing any provisioning path.\n\n## Entry points and validation\n\n`src/sveltekit/` owns `createSessionHandler`, `createSessionCookie`,\n`destroySessionCookie`, and `switchSessionTenant`. Use README integration examples\nrather than copying application setup into these instructions.\n\nFrom the repository root:\n\n```bash\npnpm --filter @happyvertical/smrt-users test\npnpm --filter @happyvertical/smrt-users typecheck\npnpm --filter @happyvertical/smrt-users test:postgres\n```\n\nStart with the relevant test file via `test -- src/__tests__/<file>.test.ts`.\nRun `test:postgres` for RLS, principal context, OIDC, or terminal-auth database\nchanges. `typecheck` includes Svelte accessibility checks; plain `tsc` is\ninsufficient. Follow root knowledge freshness checks before shipping.\n",
10552
10559
  "moduleDocs": [
10553
10560
  {
10554
10561
  "path": "agents/permissions.md",
10555
10562
  "module": "permissions",
10556
- "content": "# Permissions and tenant inheritance\n\nRead for `src/services/PermissionResolver.ts`, operation guards, role seeding,\nRLS, and tenant-hierarchy changes. Integration examples live in README's\nmanifest-derived permission catalog and PostgreSQL RLS sections.\n\n## Membership selection\n\n1. A direct target-tenant membership pins resolution: active resolves normally;\n pending/suspended returns empty. Never union direct and ancestor roles.\n2. Otherwise walk hierarchyPath nearest-first for the nearest active ancestor\n membership whose role has inheritsToDescendants. Skip inactive/unflagged\n ancestors; they neither grant nor block. No qualifying ancestor means empty.\n3. Run later layers against the target tenant. Only target-tenant groups apply;\n membership overrides travel with the chosen ancestor membership.\n inheritedFromTenantId reports that ancestor, null for direct resolution.\n\nVerify hierarchyPath link-by-link against loaded parentTenantId rows before\ntrusting it: excess depth, self-reference, duplicates or inconsistent paths\nfail closed. Bound traversal by MAX_TENANT_HIERARCHY_DEPTH. Tenant status is not\nconsulted, matching direct resolution. Long-lived user/tenant caches must\ninvalidate ancestor membership changes and inheritsToDescendants flips;\nrequest-scoped caches do not survive those requests.\n\n`loadSessionContext().tenantAuthorization` is authoritative for required-tenant\nconsumers; membership null can mean inherited authority.\n\n4. If no membership resolved at all, apply the opt-in ancestor-read policy\n (smrt#2939): the user's ACTIVE memberships on VERIFIED DESCENDANTS of the\n target, whose role slug is declared in `permissions.ancestorRead.roles` and\n within `maxDepth` hops, contribute `<collection>.read` for declared\n collections, intersected with BOTH that role's own catalog grants (so a\n descendant-side tenant GRANT, group role, or membership GRANT cannot widen\n it) AND the principal's effective permissions in that descendant (so its\n DENY cascade and membership DENY apply). Only a SYSTEM\n role (`tenantId` null, `isSystem: true`) matches a declared slug: slugs are\n not unique across a hierarchy, so a tenant-scoped custom role of the same\n name must not opt its tenant in. Off by default;\n read-only; never lateral; never reached when a direct or inherited\n membership already decided. The target tenant's DENY still subtracts.\n `ancestorReadFromTenantIds` reports the contributing descendants. The grant\n carries no membership, so `membershipId`/`tenantAuthorization.membershipId`\n stay null and a consumer requiring a non-empty membership still refuses.\n\n## Permission precedence\n\nApply these layers in order, later layers overriding earlier ones:\n\n1. Ancestor TenantPermissionOverride cascade (GRANT adds, DENY removes).\n2. Selected membership role permissions.\n3. Target-tenant group-role permissions.\n4. Tenant cascade's net DENY removes role/group grants.\n5. Membership GRANT may re-add a tenant-denied slug.\n6. Membership DENY removes last and always wins.\n\nThe tenant block is the cascade's net result, not the union of historical DENYs:\na child's more-specific GRANT can override a parent DENY. Use a direct child\nmembership or tenant DENY to attenuate inherited role authority.\n`getGroupIdsForTenant(userId, tenantId)` is required; getGroupIds is cross-tenant.\n\n## Hierarchies and seeding\n\n- createChild calculates paths/depth; moveToParent updates all descendants;\n getTree(rootId?) returns UI structure. Maximum depth is 10.\n- Tenant override cascade requires parent cascadePermissions and child\n inheritPermissions. These flags do not gate the independent, per-role\n inheritsToDescendants membership flow.\n- `permissions.ancestorRead` in the `users` package config declares upward,\n read-only visibility: `{ roles, collections, maxDepth? }`. Both lists are\n required and non-empty and `maxDepth >= 1`, else the policy is OFF — there is\n no partially valid declaration. `isAncestorReadableSlug()` is the single place\n the read-only bound is enforced; only a two-segment `<collection>.<action>`\n slug whose action normalizes to `read` on a declared collection passes.\n `PermissionResolver.create(options, { ancestorReadPolicy })` binds one\n resolver; `null` forces it off. It grants the OPERATION at the ancestor, never\n row visibility — sibling rows stay scoped by the tenancy interceptor and RLS.\n- seedSystemRoles({ inheritsToDescendants: ['owner', 'admin'] }) flags listed\n slugs additively, never unflags omitted ones, and rejects unknown slugs.\n Default seeds are exact-tenant.\n- RolePermissionCollection.seedRolePermissions() or\n seedSystemRoles({ seedPermissions: true }) maps owner/admin to all catalog\n permissions, member to ordinary-resource read/create, viewer to read. Member\n create excludes identity/RBAC/security resources and their joins/overrides.\n Seeding is additive/idempotent; removal requires prune: true.\n- When a package adds built-in self-personalization permissions after role\n creation, explicitly call seedDefaultRolePersonalizationPermissions(). It\n upgrades owner/admin/member/viewer idempotently and never grants custom roles.\n\n## Guards and RLS\n\nPermissionCatalogService derives collection.action slugs from manifests,\nincluding custom actions; list/get map to read. Hand-written mutations in form\nhandlers, endpoints, CLI and jobs use assertOperationPermission(). It requires\ncatalog presence then resolves permissions, throwing fail-closed by default.\nUse onDeny: 'return', checkOperationPermission or hasOperationPermission only\nwhen the caller handles a structured/boolean denial.\n\nGenerated SvelteKit write routes (POST/PUT/PATCH/DELETE CRUD and mutating\ncustom actions) enforce the same `collection.action` slug (custom actions use\n`collection.<method>`; actions hosted on a collection class use the item collection, which the catalog now emits) after authentication: the slug must be in the request's\nsession permission snapshot (`locals.permissions`/`permissionSet`/\n`smrtPermissions` or `locals.tenantContext.permissions`), otherwise 403. Only\n`locals.tenantContext.superAdminBypass` skips it; `api.public: true` stays open.\n\nResource-anchored guards receive the resource tenant ID. When it differs from\nthe session tenant, omit the session membership so the resolver selects the\nappropriate authority; mismatched supplied membership/tenant fails closed.\nInherited root-admin authority can then authorize descendant resources without\napplication-side membership fan-out.\n\nSystem context and super-admin bypass are honored. Pass\nallowSuperAdminBypass: false for money or separation-of-duties operations that\nrequire an explicit grant.\n\ngeneratePostgresPermissionSql/applyPostgresPermissionPolicies enforce generated\nRLS using smrt.permissions and smrt.tenant_id, installed with set_config by\nwithSessionPermissionContext. This bounds REST, MCP and in-process database\naccess once principal context is set. Child-tenant sessions receive resolved\ninherited permissions automatically, but RLS row filtering remains bound to the\nsession tenant: a root session's app-level guard authorization does not itself\npermit child rows through RLS. Preserve this distinction.\n"
10563
+ "content": "# Permissions and tenant inheritance\n\nRead for `src/services/PermissionResolver.ts`, operation guards, role seeding,\nRLS, and tenant-hierarchy changes. Integration examples live in README's\nmanifest-derived permission catalog and PostgreSQL RLS sections.\n\n## Membership selection\n\n1. A direct target-tenant membership pins resolution: active resolves normally;\n pending/suspended returns empty. Never union direct and ancestor roles.\n2. Otherwise walk hierarchyPath nearest-first for the nearest active ancestor\n membership whose role has inheritsToDescendants. Skip inactive/unflagged\n ancestors; they neither grant nor block. No qualifying ancestor means empty.\n3. Run later layers against the target tenant. Only target-tenant groups apply;\n membership overrides travel with the chosen ancestor membership.\n inheritedFromTenantId reports that ancestor, null for direct resolution.\n\nVerify hierarchyPath link-by-link against loaded parentTenantId rows before\ntrusting it: excess depth, self-reference, duplicates or inconsistent paths\nfail closed. Bound traversal by MAX_TENANT_HIERARCHY_DEPTH. Tenant status is not\nconsulted, matching direct resolution. Long-lived user/tenant caches must\ninvalidate ancestor membership changes and inheritsToDescendants flips;\nrequest-scoped caches do not survive those requests.\n\n`loadSessionContext().tenantAuthorization` is authoritative for required-tenant\nconsumers; membership null can mean inherited authority.\n\n4. If no membership resolved at all, apply the opt-in ancestor-read policy\n (smrt#2939): the user's ACTIVE memberships on VERIFIED DESCENDANTS of the\n target, whose role slug is declared in `permissions.ancestorRead.roles` and\n within `maxDepth` hops, contribute `<collection>.read` for declared\n collections, intersected with BOTH that role's own catalog grants (so a\n descendant-side tenant GRANT, group role, or membership GRANT cannot widen\n it) AND the principal's effective permissions in that descendant (so its\n DENY cascade and membership DENY apply). Only a SYSTEM\n role (`tenantId` null, `isSystem: true`) matches a declared slug: slugs are\n not unique across a hierarchy, so a tenant-scoped custom role of the same\n name must not opt its tenant in. Off by default;\n read-only; never lateral; never reached when a direct or inherited\n membership already decided. The target tenant's DENY still subtracts.\n `ancestorReadFromTenantIds` reports the contributing descendants. The grant\n carries no membership, so `membershipId`/`tenantAuthorization.membershipId`\n stay null and a consumer requiring a non-empty membership still refuses.\n\n## Permission precedence\n\nApply these layers in order, later layers overriding earlier ones:\n\n1. Ancestor TenantPermissionOverride cascade (GRANT adds, DENY removes).\n2. Selected membership role permissions.\n3. Target-tenant group-role permissions.\n4. Tenant cascade's net DENY removes role/group grants.\n5. Membership GRANT may re-add a tenant-denied slug.\n6. Membership DENY removes last and always wins.\n\nThe tenant block is the cascade's net result, not the union of historical DENYs:\na child's more-specific GRANT can override a parent DENY. Use a direct child\nmembership or tenant DENY to attenuate inherited role authority.\n`getGroupIdsForTenant(userId, tenantId)` is required; getGroupIds is cross-tenant.\n\n## Hierarchies and seeding\n\n- `Tenant.save()` derives hierarchyPath/hierarchyLevel from the REAL\n parentTenantId chain on every save (never from an ancestor's stored path),\n refuses missing parents, cycles and over-depth before writing, and\n re-materializes descendants by raw column update (so STI subclass columns\n are never clobbered). createChild/moveToParent/makeRoot are thin wrappers.\n `materializeTenantHierarchy(db, { dryRun })` / `smrt\n db:materialize-tenant-hierarchy` backfills legacy rows: idempotent, one\n transaction, refuses a broken chain without writing (#3036). A NEW or\n CHANGED parent link is authority-bearing, so `Tenant.save()` first loads the\n parent through TenantCollection (the caller's tenancy scope) and refuses an\n invisible one on every path — helpers, `create({ parentTenantId })`, or a\n plain assignment + save; system context bypasses as usual, and an unchanged\n link is never re-checked. getTree(rootId?)\n returns UI structure. Maximum depth is 10.\n- Tenant override cascade requires parent cascadePermissions and child\n inheritPermissions. These flags do not gate the independent, per-role\n inheritsToDescendants membership flow.\n- `permissions.ancestorRead` in the `users` package config declares upward,\n read-only visibility: `{ roles, collections, maxDepth? }`. Both lists are\n required and non-empty and `maxDepth >= 1`, else the policy is OFF — there is\n no partially valid declaration. `isAncestorReadableSlug()` is the single place\n the read-only bound is enforced; only a two-segment `<collection>.<action>`\n slug whose action normalizes to `read` on a declared collection passes.\n `PermissionResolver.create(options, { ancestorReadPolicy })` binds one\n resolver; `null` forces it off. It grants the OPERATION at the ancestor, never\n row visibility — sibling rows stay scoped by the tenancy interceptor and RLS.\n- seedSystemRoles({ inheritsToDescendants: ['owner', 'admin'] }) flags listed\n slugs additively, never unflags omitted ones, and rejects unknown slugs.\n Default seeds are exact-tenant.\n- RolePermissionCollection.seedRolePermissions() or\n seedSystemRoles({ seedPermissions: true }) maps owner/admin to all catalog\n permissions, member to ordinary-resource read/create, viewer to read. Member\n create excludes identity/RBAC/security resources and their joins/overrides.\n Seeding is additive/idempotent; removal requires prune: true.\n- When a package adds built-in self-personalization permissions after role\n creation, explicitly call seedDefaultRolePersonalizationPermissions(). It\n upgrades owner/admin/member/viewer idempotently and never grants custom roles.\n\n## Guards and RLS\n\nPermissionCatalogService derives collection.action slugs from manifests,\nincluding custom actions; list/get map to read. Hand-written mutations in form\nhandlers, endpoints, CLI and jobs use assertOperationPermission(). It requires\ncatalog presence then resolves permissions, throwing fail-closed by default.\nUse onDeny: 'return', checkOperationPermission or hasOperationPermission only\nwhen the caller handles a structured/boolean denial.\n\nGenerated SvelteKit write routes (POST/PUT/PATCH/DELETE CRUD and mutating\ncustom actions) enforce the same `collection.action` slug (custom actions use\n`collection.<method>`; actions hosted on a collection class use the item collection, which the catalog now emits) after authentication: the slug must be in the request's\nsession permission snapshot (`locals.permissions`/`permissionSet`/\n`smrtPermissions` or `locals.tenantContext.permissions`), otherwise 403. Only\n`locals.tenantContext.superAdminBypass` skips it; `api.public: true` stays open.\n\nResource-anchored guards receive the resource tenant ID. When it differs from\nthe session tenant, omit the session membership so the resolver selects the\nappropriate authority; mismatched supplied membership/tenant fails closed.\nInherited root-admin authority can then authorize descendant resources without\napplication-side membership fan-out.\n\nPermissionResolver runs every resolution inside smrt-tenancy's system context\n(#3036). Its reads are deliberately cross-tenant and framework-owned — the\nancestor TenantPermissionOverride batch, ancestor memberships for\ninheritsToDescendants, descendant memberships for ancestor-read — and are keyed\nby the explicit (userId, tenantId); with those classes registered tenant-scoped\n(`autoFilter`) the consumer's filter otherwise threw TenantIsolationError or\nsilently narrowed them. It returns slugs and ids only, never rows, and the\ncaller's own reads stay filtered. Do not move a resolver read back under the\nambient tenant; the Postgres regression suite\n`issue-3036-hierarchy-resolution-postgres.test.ts` pins this. Because nothing\nbackstops those reads any more, the tenant-override cascade verifies its chain\ntoo: it uses hierarchyPath only when it agrees link-by-link with\nparentTenantId, otherwise walks the real parent links (so a forged path cannot\ncascade an unrelated tenant in, and a never-materialized row still gets its real\nancestors' DENYs — and GRANTs), and throws TenantHierarchyError on a broken\nreal chain: a per-request failure for that tenant's whole subtree, which\n`smrt db:materialize-tenant-hierarchy --dry-run` reports ahead of time (see\nREADME \"Upgrading an existing fleet\").\n`getTenantInheritanceChain()` returns Tenant rows, so it is NOT run in system\ncontext; it reports the same verified chain the cascade applies when the\ncaller can read every ancestor. Otherwise it never reports PARENT_NOT_FOUND\nfor a row the caller simply cannot see: an interceptor refusal surfaces as\nTenantIsolationError, and a silently filtered row (e.g. RLS) ends the chain at\nthe nearest readable ancestor. Descendant path rewrites are raw column updates recorded through\n`bumpChangeFeed`; `updated_at` is intentionally not bumped (derived columns;\nevery save recomputes them). They are not atomic with the moving row's save —\nan interrupted subtree fails closed until re-saved or backfilled.\n\nSystem context and super-admin bypass are honored. Pass\nallowSuperAdminBypass: false for money or separation-of-duties operations that\nrequire an explicit grant.\n\ngeneratePostgresPermissionSql/applyPostgresPermissionPolicies enforce generated\nRLS using smrt.permissions and smrt.tenant_id, installed with set_config by\nwithSessionPermissionContext. This bounds REST, MCP and in-process database\naccess once principal context is set. Child-tenant sessions receive resolved\ninherited permissions automatically, but RLS row filtering remains bound to the\nsession tenant: a root session's app-level guard authorization does not itself\npermit child rows through RLS. Preserve this distinction.\n"
10557
10564
  },
10558
10565
  {
10559
10566
  "path": "agents/oidc-provisioning.md",
package/dist/sveltekit.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as decodeOidcTransaction, E as resolveOidcProviderConfig, S as OidcLoginService, T as getUsersOidcConfig, _ as readMobileBearerToken, a as TerminalAuthRateLimitError, g as MobileAuthService, h as MobileAuthError, i as TerminalAuthError, m as withSessionPermissionContext, o as TerminalAuthService, s as OperationPermissionError, v as validateMobileRedirectUri, w as encodeOidcTransaction, x as OidcLoginError, y as SessionService } from "./chunks/TerminalAuthService-DgAU4Fz1.js";
1
+ import { C as decodeOidcTransaction, E as resolveOidcProviderConfig, S as OidcLoginService, T as getUsersOidcConfig, _ as readMobileBearerToken, a as TerminalAuthRateLimitError, g as MobileAuthService, h as MobileAuthError, i as TerminalAuthError, m as withSessionPermissionContext, o as TerminalAuthService, s as OperationPermissionError, v as validateMobileRedirectUri, w as encodeOidcTransaction, x as OidcLoginError, y as SessionService } from "./chunks/TerminalAuthService-BBxkYCOG.js";
2
2
  import { DiscoveryArtifactValidationError, SMRT_APP_RESULT_CONTRACT, SMRT_APP_RESULT_SCHEMA, SMRT_APP_RESULT_VERSION, SMRT_DISCOVERY_CONFORMANCE_ARTIFACT_SCHEMA, SMRT_DISCOVERY_CONFORMANCE_SCHEMA, SMRT_DISCOVERY_CONFORMANCE_VERSION, SMRT_MCP_RESULT_METADATA_KEY, canonicalizeDiscoveryArtifact, createDiscoveryConformanceArtifact, deriveCommandRequirements, validateDiscoveryConformanceArtifact } from "./app-contract.js";
3
3
  import { ObjectRegistry, createClassNamePredicate, resolveCustomActionMetadata, resolveEffectiveActionMetadata } from "@happyvertical/smrt-core";
4
4
  import { createLogger } from "@happyvertical/logger";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-users",
3
- "version": "0.51.14",
3
+ "version": "0.51.16",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "Multi-tenant user management for the SMRT framework - users, tenants, roles, permissions, groups",
6
6
  "type": "module",
@@ -47,18 +47,18 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@happyvertical/logger": "^0.89.12",
50
- "@happyvertical/smrt-config": "0.51.14",
51
- "@happyvertical/smrt-core": "0.51.14",
52
- "@happyvertical/smrt-mobile-contract": "0.51.14",
53
- "@happyvertical/smrt-profiles": "0.51.14",
54
- "@happyvertical/smrt-tenancy": "0.51.14",
55
- "@happyvertical/smrt-types": "0.51.14",
56
- "@happyvertical/smrt-ui": "0.51.14",
50
+ "@happyvertical/smrt-config": "0.51.16",
51
+ "@happyvertical/smrt-core": "0.51.16",
52
+ "@happyvertical/smrt-mobile-contract": "0.51.16",
53
+ "@happyvertical/smrt-profiles": "0.51.16",
54
+ "@happyvertical/smrt-tenancy": "0.51.16",
55
+ "@happyvertical/smrt-types": "0.51.16",
56
+ "@happyvertical/smrt-ui": "0.51.16",
57
57
  "@happyvertical/sql": "^0.89.12",
58
58
  "jose": "^6.2.12"
59
59
  },
60
60
  "devDependencies": {
61
- "@happyvertical/smrt-vitest": "0.51.14",
61
+ "@happyvertical/smrt-vitest": "0.51.16",
62
62
  "@sveltejs/package": "^2.5.8",
63
63
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
64
64
  "@types/node": "24.13.6",
@@ -96,7 +96,7 @@
96
96
  "clean": "rm -rf dist",
97
97
  "dev": "npm run build:watch",
98
98
  "test": "vitest run",
99
- "test:postgres": "node ../../scripts/run-with-ci-postgres.mjs -- pnpm exec vitest run src/__tests__/permission-postgres-rls.test.ts src/__tests__/principal-permission-context-postgres.test.ts src/__tests__/oidc-provisioning-postgres.test.ts src/__tests__/legacy-user-profile-backfill-postgres.test.ts src/__tests__/terminal-auth-postgres.test.ts src/__tests__/resource-grant-postgres.test.ts",
99
+ "test:postgres": "node ../../scripts/run-with-ci-postgres.mjs -- pnpm exec vitest run src/__tests__/permission-postgres-rls.test.ts src/__tests__/principal-permission-context-postgres.test.ts src/__tests__/oidc-provisioning-postgres.test.ts src/__tests__/legacy-user-profile-backfill-postgres.test.ts src/__tests__/terminal-auth-postgres.test.ts src/__tests__/resource-grant-postgres.test.ts src/__tests__/issue-3036-hierarchy-resolution-postgres.test.ts",
100
100
  "typecheck": "tsc --noEmit && node ../../scripts/svelte-check-a11y.mjs --tsconfig ./tsconfig.svelte.json",
101
101
  "check": "pnpm exec svelte-check --tsconfig ./tsconfig.svelte.json",
102
102
  "verify:pack": "node ../../scripts/verify-package-types-exports.js ."