@happyvertical/smrt-users 0.51.9 → 0.51.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -0
- package/README.md +96 -1
- package/dist/chunks/{TerminalAuthService-BfZmKdZW.js → TerminalAuthService-BlDTirYc.js} +197 -8
- package/dist/chunks/TerminalAuthService-BlDTirYc.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/services/AncestorReadPolicy.d.ts +143 -0
- package/dist/services/AncestorReadPolicy.d.ts.map +1 -0
- package/dist/services/PermissionResolver.d.ts +105 -2
- package/dist/services/PermissionResolver.d.ts.map +1 -1
- package/dist/services/index.d.ts +2 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +7 -7
- package/dist/sveltekit.js +1 -1
- package/package.json +9 -9
- package/dist/chunks/TerminalAuthService-BfZmKdZW.js.map +0 -1
package/dist/manifest.json
CHANGED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declared, read-only ancestor visibility policy.
|
|
3
|
+
*
|
|
4
|
+
* A membership is normally authority DOWNWARD only: a direct membership row in
|
|
5
|
+
* the target tenant, or — opt-in, per role — the nearest ACTIVE ancestor
|
|
6
|
+
* membership whose role is flagged `inheritsToDescendants`. Nothing a user
|
|
7
|
+
* holds on a DESCENDANT tenant contributes anything when the permission
|
|
8
|
+
* context is resolved at an ancestor, so a principal whose only membership is
|
|
9
|
+
* on a child tenant resolves to the empty set at the root (smrt#2939).
|
|
10
|
+
*
|
|
11
|
+
* That is correct by default — a descendant membership is narrower authority
|
|
12
|
+
* than its ancestor — but it prevents a legitimate, common shape: a
|
|
13
|
+
* network-level LIST that members of the network's child tenants are meant to
|
|
14
|
+
* read. This module describes the narrow, declared exception.
|
|
15
|
+
*
|
|
16
|
+
* Everything about it is deliberately restrictive:
|
|
17
|
+
*
|
|
18
|
+
* - **Off by default.** An application that declares no policy resolves
|
|
19
|
+
* exactly as before.
|
|
20
|
+
* - **Declared, never inferred.** The application names the descendant role
|
|
21
|
+
* slugs and the collections. Nothing is derived from role names, catalog
|
|
22
|
+
* shape, or hierarchy position. Because a slug is not unique across a
|
|
23
|
+
* hierarchy — tenant-scoped custom roles are created by whoever administers
|
|
24
|
+
* that tenant — only a governed SYSTEM role (`tenantId` null and
|
|
25
|
+
* `isSystem: true`, what `RoleCollection.seedSystemRoles()` creates) can
|
|
26
|
+
* match a declared slug. A descendant cannot opt itself in by minting a
|
|
27
|
+
* same-named custom role.
|
|
28
|
+
* - **Read only.** Only `<collection>.read` is ever contributed (`list`/`get`
|
|
29
|
+
* normalize to `read`). No `create`/`update`/`delete`, no custom action.
|
|
30
|
+
* - **Never an escalation, in either direction.** The contribution is
|
|
31
|
+
* intersected with BOTH the declared role's own catalog grants AND the
|
|
32
|
+
* principal's effective permissions in the contributing tenant. The role
|
|
33
|
+
* bound keeps the contribution inside what the ancestor declared, so a
|
|
34
|
+
* descendant tenant's administrator cannot widen it with a tenant GRANT, a
|
|
35
|
+
* group role, or a membership GRANT. The effective bound means a DENY that
|
|
36
|
+
* removed a permission at home removes it at the ancestor too.
|
|
37
|
+
* - **Never lateral.** The policy grants the OPERATION at the ancestor. It is
|
|
38
|
+
* NOT visibility of a sibling tenant's rows — row scoping remains the
|
|
39
|
+
* executor's job (the `@happyvertical/smrt-tenancy` interceptor and the
|
|
40
|
+
* generated Postgres RLS policies), and a sibling's rows stay unreadable.
|
|
41
|
+
*
|
|
42
|
+
* @packageDocumentation
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* Default number of hierarchy hops a descendant membership may travel upward
|
|
46
|
+
* when the policy does not say. `1` is the immediate parent only.
|
|
47
|
+
*/
|
|
48
|
+
export declare const DEFAULT_ANCESTOR_READ_MAX_DEPTH = 1;
|
|
49
|
+
/**
|
|
50
|
+
* The only action an ancestor-read grant can ever carry.
|
|
51
|
+
*/
|
|
52
|
+
export declare const ANCESTOR_READ_ACTION = "read";
|
|
53
|
+
/**
|
|
54
|
+
* Declared ancestor-read policy.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* // smrt.config.ts
|
|
59
|
+
* export default defineConfig({
|
|
60
|
+
* packages: {
|
|
61
|
+
* users: {
|
|
62
|
+
* permissions: {
|
|
63
|
+
* ancestorRead: {
|
|
64
|
+
* roles: ['member', 'editor'],
|
|
65
|
+
* collections: ['publications', 'tenants'],
|
|
66
|
+
* maxDepth: 2,
|
|
67
|
+
* },
|
|
68
|
+
* },
|
|
69
|
+
* },
|
|
70
|
+
* },
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export interface AncestorReadPolicy {
|
|
75
|
+
/**
|
|
76
|
+
* Descendant role slugs whose memberships may contribute upward. Required
|
|
77
|
+
* and non-empty: an omitted or empty list disables the policy entirely.
|
|
78
|
+
* Matching is exact and case-insensitive on the role slug; no patterns.
|
|
79
|
+
*
|
|
80
|
+
* Only SYSTEM roles match — `tenantId` null and `isSystem: true`, as created
|
|
81
|
+
* by `RoleCollection.seedSystemRoles()`. A tenant-scoped custom role sharing
|
|
82
|
+
* the slug contributes nothing, so a descendant tenant's administrator
|
|
83
|
+
* cannot mint its way into an ancestor's allow-list.
|
|
84
|
+
*/
|
|
85
|
+
roles: readonly string[];
|
|
86
|
+
/**
|
|
87
|
+
* Collection slugs whose `read` permission may travel upward. Required and
|
|
88
|
+
* non-empty. `*` is a wildcard matching any run of characters and may appear
|
|
89
|
+
* anywhere in the pattern (`'site_*'`, `'*_pages'`, `'a*b'`); `'*'` alone
|
|
90
|
+
* means every collection the declared role can already read. Matching is
|
|
91
|
+
* case-insensitive and applies to the collection segment only.
|
|
92
|
+
*/
|
|
93
|
+
collections: readonly string[];
|
|
94
|
+
/**
|
|
95
|
+
* Maximum number of hierarchy hops from the descendant membership up to the
|
|
96
|
+
* tenant being resolved. Defaults to {@link DEFAULT_ANCESTOR_READ_MAX_DEPTH}
|
|
97
|
+
* (immediate parent only). Values below 1 disable the policy.
|
|
98
|
+
*/
|
|
99
|
+
maxDepth?: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The `permissions.ancestorRead` slice of the `users` package config.
|
|
103
|
+
*/
|
|
104
|
+
export interface AncestorReadPackageConfig extends Record<string, unknown> {
|
|
105
|
+
permissions?: {
|
|
106
|
+
ancestorRead?: AncestorReadPolicy;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A validated policy: slugs lowercased and de-duplicated, depth clamped.
|
|
111
|
+
*/
|
|
112
|
+
export interface NormalizedAncestorReadPolicy {
|
|
113
|
+
roleSlugs: ReadonlySet<string>;
|
|
114
|
+
collectionPatterns: readonly string[];
|
|
115
|
+
maxDepth: number;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Validate a declared policy. Returns `null` — the policy is OFF — whenever it
|
|
119
|
+
* is absent, malformed, empty on either axis, or bounded to zero depth. There
|
|
120
|
+
* is no partially-valid policy: an unusable declaration fails closed rather
|
|
121
|
+
* than grants something the application did not fully describe.
|
|
122
|
+
*/
|
|
123
|
+
export declare function normalizeAncestorReadPolicy(policy: AncestorReadPolicy | null | undefined): NormalizedAncestorReadPolicy | null;
|
|
124
|
+
/**
|
|
125
|
+
* Read the declared policy from the `users` package config. Returns `null`
|
|
126
|
+
* when nothing is declared — the default for every existing application.
|
|
127
|
+
*/
|
|
128
|
+
export declare function getConfiguredAncestorReadPolicy(): NormalizedAncestorReadPolicy | null;
|
|
129
|
+
/**
|
|
130
|
+
* Decide whether a permission slug offered by the caller may travel upward
|
|
131
|
+
* under this policy.
|
|
132
|
+
*
|
|
133
|
+
* A slug qualifies only when it is a `<collection>.<action>` pair whose action
|
|
134
|
+
* normalizes to `read` and whose collection matches a declared pattern. A slug
|
|
135
|
+
* with no action segment, extra segments, or any non-read action is rejected —
|
|
136
|
+
* this is the single place the read-only invariant is enforced, so a write
|
|
137
|
+
* permission cannot reach an ancestor through any declaration. The caller is
|
|
138
|
+
* responsible for the separate bounds on WHICH slugs are offered here (the
|
|
139
|
+
* declared role's own grants, intersected with what the principal effectively
|
|
140
|
+
* holds in the contributing tenant).
|
|
141
|
+
*/
|
|
142
|
+
export declare function isAncestorReadableSlug(slug: string, policy: NormalizedAncestorReadPolicy): boolean;
|
|
143
|
+
//# sourceMappingURL=AncestorReadPolicy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AncestorReadPolicy.d.ts","sourceRoot":"","sources":["../../src/services/AncestorReadPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAKH;;;GAGG;AACH,eAAO,MAAM,+BAA+B,IAAI,CAAC;AAEjD;;GAEG;AACH,eAAO,MAAM,oBAAoB,SAAS,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;OASG;IACH,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB;;;;;;OAMG;IACH,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,yBAA0B,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACxE,WAAW,CAAC,EAAE;QACZ,YAAY,CAAC,EAAE,kBAAkB,CAAC;KACnC,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC3C,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC/B,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAiBD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,kBAAkB,GAAG,IAAI,GAAG,SAAS,GAC5C,4BAA4B,GAAG,IAAI,CAyBrC;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,IAAI,4BAA4B,GAAG,IAAI,CAGrF;AAgBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,4BAA4B,GACnC,OAAO,CAkBT"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SmrtClassOptions } from '@happyvertical/smrt-core';
|
|
2
2
|
import { Membership } from '../models/Membership.js';
|
|
3
3
|
import { Tenant } from '../models/Tenant.js';
|
|
4
|
+
import { AncestorReadPolicy } from './AncestorReadPolicy.js';
|
|
4
5
|
/**
|
|
5
6
|
* Permission resolution result
|
|
6
7
|
*/
|
|
@@ -22,6 +23,31 @@ export interface PermissionResolutionResult {
|
|
|
22
23
|
* `inheritsToDescendants: true`). `null` for direct-membership resolution.
|
|
23
24
|
*/
|
|
24
25
|
inheritedFromTenantId: string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Descendant tenant ids whose memberships contributed declared, read-only
|
|
28
|
+
* permissions under the opt-in ancestor-read policy (smrt#2939). Empty for
|
|
29
|
+
* every resolution that did not use the policy — including every resolution
|
|
30
|
+
* in an application that declares none.
|
|
31
|
+
*
|
|
32
|
+
* These ids report WHY a read operation is authorized at this tenant. They
|
|
33
|
+
* are NOT a row-visibility grant for those tenants: the resolved permission
|
|
34
|
+
* is the operation at the tenant being resolved, and row scoping stays with
|
|
35
|
+
* the tenancy interceptor and RLS.
|
|
36
|
+
*/
|
|
37
|
+
ancestorReadFromTenantIds: string[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Construction-time options for {@link PermissionResolver}.
|
|
41
|
+
*/
|
|
42
|
+
export interface PermissionResolverOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Declared ancestor-read policy. When omitted (the default), the resolver
|
|
45
|
+
* reads `packages.users.permissions.ancestorRead` from the application
|
|
46
|
+
* config; when explicitly `null`, the policy is forced OFF regardless of
|
|
47
|
+
* configuration. Pass a literal policy to bind one resolver without
|
|
48
|
+
* touching global config (tests, embedded runtimes).
|
|
49
|
+
*/
|
|
50
|
+
ancestorReadPolicy?: AncestorReadPolicy | null;
|
|
25
51
|
}
|
|
26
52
|
export interface PermissionResolutionOptions {
|
|
27
53
|
/**
|
|
@@ -95,6 +121,20 @@ export interface TenantPermissionInheritanceResult {
|
|
|
95
121
|
* overrides still subtract from inherited grants, and with no flagged role
|
|
96
122
|
* the resolver behaves exactly as before. See `resolvePermissions`.
|
|
97
123
|
*
|
|
124
|
+
* ## Declared read-only ancestor visibility (opt-in, smrt#2939)
|
|
125
|
+
*
|
|
126
|
+
* Both flows above move authority DOWN. A membership held on a DESCENDANT
|
|
127
|
+
* contributes nothing at an ancestor, so a principal whose only membership is
|
|
128
|
+
* on a child tenant resolves to the empty set at the root. When an application
|
|
129
|
+
* declares `packages.users.permissions.ancestorRead`, such a principal
|
|
130
|
+
* additionally receives the declared `<collection>.read` slugs at the
|
|
131
|
+
* ancestor — read only, listed roles and collections only, bounded by
|
|
132
|
+
* `maxDepth`, intersected with what the descendant role already holds, and
|
|
133
|
+
* only when NO membership authorized the tenant at all. It is never lateral:
|
|
134
|
+
* it authorizes the operation at the ancestor, not visibility of a sibling
|
|
135
|
+
* tenant's rows, which the tenancy interceptor and RLS keep scoped. Off by
|
|
136
|
+
* default. See {@link AncestorReadPolicy}.
|
|
137
|
+
*
|
|
98
138
|
* @example
|
|
99
139
|
* ```typescript
|
|
100
140
|
* const resolver = new PermissionResolver(options);
|
|
@@ -113,6 +153,7 @@ export interface TenantPermissionInheritanceResult {
|
|
|
113
153
|
*/
|
|
114
154
|
export declare class PermissionResolver {
|
|
115
155
|
private options;
|
|
156
|
+
private readonly ancestorReadPolicyOverride;
|
|
116
157
|
private membershipCollection;
|
|
117
158
|
private roleCollection;
|
|
118
159
|
private rolePermissionCollection;
|
|
@@ -122,7 +163,18 @@ export declare class PermissionResolver {
|
|
|
122
163
|
private permissionCollection;
|
|
123
164
|
private tenantCollection;
|
|
124
165
|
private tenantPermissionOverrideCollection;
|
|
125
|
-
constructor(options: SmrtClassOptions);
|
|
166
|
+
constructor(options: SmrtClassOptions, resolverOptions?: PermissionResolverOptions);
|
|
167
|
+
/**
|
|
168
|
+
* The effective ancestor-read policy, or `null` when the feature is off.
|
|
169
|
+
*
|
|
170
|
+
* Resolved per call rather than cached on the instance so a configuration
|
|
171
|
+
* change (or a test's `setConfig`) takes effect without rebuilding long-lived
|
|
172
|
+
* resolvers. Resolution itself is uncached — the resolver reads live rows on
|
|
173
|
+
* every call — so there is no permission cache to invalidate when a
|
|
174
|
+
* membership, role, or the policy itself changes; request-scoped contexts
|
|
175
|
+
* pick up the new answer on their next resolution.
|
|
176
|
+
*/
|
|
177
|
+
private getAncestorReadPolicy;
|
|
126
178
|
/**
|
|
127
179
|
* Initialize collections
|
|
128
180
|
*
|
|
@@ -193,6 +245,57 @@ export declare class PermissionResolver {
|
|
|
193
245
|
* 6. Subtract membership DENY overrides (absolute precedence)
|
|
194
246
|
*/
|
|
195
247
|
resolvePermissions(userId: string, tenantId: string, options?: PermissionResolutionOptions): Promise<PermissionResolutionResult>;
|
|
248
|
+
/**
|
|
249
|
+
* The permission slugs a role grants through the role-permission catalog,
|
|
250
|
+
* excluding every per-tenant, per-group, and per-membership override.
|
|
251
|
+
*/
|
|
252
|
+
private getRolePermissionSlugs;
|
|
253
|
+
/**
|
|
254
|
+
* Contribute declared, read-only permissions from the user's memberships on
|
|
255
|
+
* DESCENDANTS of the tenant being resolved (smrt#2939).
|
|
256
|
+
*
|
|
257
|
+
* Reached only when the user has neither a direct membership in `tenantId`
|
|
258
|
+
* nor an inheritable ancestor membership — so this never competes with, or
|
|
259
|
+
* re-adds to, an authority decision already made. With no declared policy
|
|
260
|
+
* (the default) it returns the untouched empty result, which is exactly the
|
|
261
|
+
* pre-policy behavior.
|
|
262
|
+
*
|
|
263
|
+
* What it grants and what it does NOT:
|
|
264
|
+
*
|
|
265
|
+
* - It grants the `<collection>.read` OPERATION at `tenantId`, intersected
|
|
266
|
+
* with BOTH the declared role's own catalog grants AND the principal's
|
|
267
|
+
* effective permissions in the contributing tenant. The role bound keeps
|
|
268
|
+
* the contribution inside what the ancestor declared, so a descendant
|
|
269
|
+
* administrator cannot widen it with a tenant GRANT, a group role, or a
|
|
270
|
+
* membership GRANT; the effective bound makes a membership DENY or a
|
|
271
|
+
* descendant-tenant DENY effective here too. Nothing that is not a `read`
|
|
272
|
+
* on a declared collection can pass ({@link isAncestorReadableSlug}).
|
|
273
|
+
* - It does NOT grant visibility of any tenant's rows. A principal reading a
|
|
274
|
+
* tenant-scoped collection is still filtered by the tenancy interceptor and
|
|
275
|
+
* Postgres RLS to the tenant its context is bound to, so a member of child
|
|
276
|
+
* A authorized at the root still cannot read sibling child B's rows. Row
|
|
277
|
+
* scoping is the executor's job; this is authorization only.
|
|
278
|
+
* - It is never lateral: only a STRICT ANCESTOR of the membership's tenant,
|
|
279
|
+
* within `maxDepth` hops, is affected. A sibling shares no such
|
|
280
|
+
* relationship and is unreachable by construction.
|
|
281
|
+
* - Only a governed SYSTEM role (`tenantId` null, `isSystem: true`) can
|
|
282
|
+
* contribute, so a descendant tenant cannot opt itself in by minting a
|
|
283
|
+
* custom role whose slug collides with a declared one.
|
|
284
|
+
*
|
|
285
|
+
* A tenant-level DENY on the resolved tenant still subtracts, keeping the
|
|
286
|
+
* tenant's own hard block authoritative over an inherited read.
|
|
287
|
+
*/
|
|
288
|
+
private applyAncestorReadPolicy;
|
|
289
|
+
/**
|
|
290
|
+
* True when `candidate` is a verified STRICT descendant of `ancestorId`,
|
|
291
|
+
* no more than `maxDepth` hops below it.
|
|
292
|
+
*
|
|
293
|
+
* Uses the materialized `hierarchyPath` to locate the relationship, then
|
|
294
|
+
* proves it by loading the whole chain and requiring an unbroken
|
|
295
|
+
* `parentTenantId` link root -> ... -> candidate. Over-deep, self-
|
|
296
|
+
* referential, duplicated, or inconsistent paths return false.
|
|
297
|
+
*/
|
|
298
|
+
private isVerifiedDescendantOf;
|
|
196
299
|
/**
|
|
197
300
|
* Find the membership to resolve through when the user has no direct
|
|
198
301
|
* membership row in the target tenant.
|
|
@@ -229,6 +332,6 @@ export declare class PermissionResolver {
|
|
|
229
332
|
/**
|
|
230
333
|
* Static factory method
|
|
231
334
|
*/
|
|
232
|
-
static create(options: SmrtClassOptions): Promise<PermissionResolver>;
|
|
335
|
+
static create(options: SmrtClassOptions, resolverOptions?: PermissionResolverOptions): Promise<PermissionResolver>;
|
|
233
336
|
}
|
|
234
337
|
//# sourceMappingURL=PermissionResolver.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PermissionResolver.d.ts","sourceRoot":"","sources":["../../src/services/PermissionResolver.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAUjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAA8B,KAAK,MAAM,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"PermissionResolver.d.ts","sourceRoot":"","sources":["../../src/services/PermissionResolver.ts"],"names":[],"mappings":"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"}
|
package/dist/services/index.d.ts
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
* @packageDocumentation
|
|
4
4
|
*/
|
|
5
5
|
export { ACCESS_REQUEST_CAPABILITIES, type AccessRequestAuthorizationContext, type AccessRequestAuthorizer, type AccessRequestCapability, AccessRequestError, type AccessRequestErrorCode, type AccessRequestEvent, type AccessRequestEventHandler, type AccessRequestEventType, AccessRequestService, type AccessRequestServiceOptions, type ApproveAccessRequestOptions, type CancelAccessRequestOptions, type CreateAccessRequestInput, type DeclineAccessRequestOptions, type GraduateAccessRequestOptions, type GraduateAccessRequestResult, type GraduateExistingTenantOption, type GraduateNewTenantOption, type GraduateTenantOption, type ListAccessRequestsFilter, } from './AccessRequestService.js';
|
|
6
|
+
export { ANCESTOR_READ_ACTION, type AncestorReadPackageConfig, type AncestorReadPolicy, DEFAULT_ANCESTOR_READ_MAX_DEPTH, getConfiguredAncestorReadPolicy, isAncestorReadableSlug, type NormalizedAncestorReadPolicy, normalizeAncestorReadPolicy, } from './AncestorReadPolicy.js';
|
|
6
7
|
export { MagicLinkError, type MagicLinkResult, MagicLinkService, type MagicLinkServiceOptions, type MagicLinkVerifyResult, } from './MagicLinkService.js';
|
|
7
8
|
export { MobileAuthError, type MobileAuthErrorCode, MobileAuthService, type MobileAuthServiceOptions, type MobileBootstrapContext, type MobileLoginContext, type MobileLogoutResult, type MobileRequestMeta, type MobileResolvedUser, type MobileTenantContext, readMobileBearerToken, validateMobileRedirectUri, } from './MobileAuthService.js';
|
|
8
9
|
export { type CreateAuthorizationUrlOptions, decodeOidcTransaction, encodeOidcTransaction, getUsersOidcConfig, type OidcCallbackResult, OidcLoginError, type OidcLoginResult, OidcLoginService, type OidcLoginServiceOptions, type OidcProviderConfig, type OidcProviderKind, type OidcProviderMetadata, type OidcProviderResolution, type OidcProviderResolutionOptions, type OidcTokenEndpointAuthMethod, type OidcTokenSet, type OidcTransaction, type ResolvedOidcProviderConfig, resolveOidcProviderConfig, type UsersOidcConfig, } from './OidcLoginService.js';
|
|
9
10
|
export { assertOperationPermission, checkOperationPermission, hasOperationPermission, type OperationPermissionAllowReason, type OperationPermissionDecision, type OperationPermissionDenyReason, OperationPermissionError, type OperationPermissionOptions, } from './OperationPermissionService.js';
|
|
10
11
|
export { deriveOperationPermissionCollectionName, deriveOperationPermissionSlug, normalizeOperationPermissionAction, type OperationPermissionCollectionInput, type PermissionCatalog, PermissionCatalogService, type PermissionCatalogSource, type PermissionCatalogSyncResult, type PermissionDefinition, type PostgresPermissionAction, type PostgresPermissionBinding, registerPermissionDefinitions, syncPermissionCatalog, type UsersConfig, } from './PermissionCatalogService.js';
|
|
11
|
-
export { type PermissionResolutionOptions, type PermissionResolutionResult, PermissionResolver, type TenantPermissionInheritanceResult, } from './PermissionResolver.js';
|
|
12
|
+
export { type PermissionResolutionOptions, type PermissionResolutionResult, PermissionResolver, type PermissionResolverOptions, type TenantPermissionInheritanceResult, } from './PermissionResolver.js';
|
|
12
13
|
export { applyPostgresPermissionPolicies, type GeneratePostgresPermissionSqlResult, generatePostgresPermissionSql, type PostgresPermissionPolicyReportItem, type PostgresPermissionPolicyTarget, } from './PostgresPermissionPolicies.js';
|
|
13
14
|
export { getCurrentSessionPermissionContext, getRequestScopedDatabase, type PrincipalPermissionRuntimeOptions, type SessionPermissionRuntimeContext, type SessionPermissionRuntimeOptions, withPrincipalPermissionContext, withSessionPermissionContext, } from './SessionPermissionContext.js';
|
|
14
15
|
export { type SessionContext, SessionService, type SessionServiceOptions, type SwitchTenantResult, } from './SessionService.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"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"}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -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.
|
|
6
|
+
"packageVersion": "0.51.10",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
13
|
-
"moduleDoc:agents/permissions.md": "
|
|
10
|
+
"manifest": "31b43b4efdd7e01f08f81d361c8d2359576aa26083fe7840dbc22834033f682c",
|
|
11
|
+
"packageJson": "bf4be91195458e06f29ce8a44274296cc1166121093ca8c86ad801895bbe26a6",
|
|
12
|
+
"agents": "0d011d9fe314ca90141f2fa45a97cdbb34dc34d0bb046357dd6d159c686bc41e",
|
|
13
|
+
"moduleDoc:agents/permissions.md": "44c316541a40ac6f7e96605241c5ef6929440322cc8bbd701d570b5184026bce",
|
|
14
14
|
"moduleDoc:agents/oidc-provisioning.md": "5efa607ff23b9a693391e0c02670324a8cea4114d4968c9718f10a763da919c6",
|
|
15
15
|
"moduleDoc:agents/mobile-auth.md": "25e85e83f43e3a46383a92fa809f3a003b4545c3026e78dbe1bccbbc35161e18",
|
|
16
16
|
"moduleDoc:agents/retention.md": "425c0a34ed33969badfef699751bfa670bdf618eda65e079e00815620aabed68"
|
|
@@ -10226,12 +10226,12 @@
|
|
|
10226
10226
|
"polymorphicAssociations": 0,
|
|
10227
10227
|
"uuidColumns": 82
|
|
10228
10228
|
},
|
|
10229
|
-
"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- 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",
|
|
10229
|
+
"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",
|
|
10230
10230
|
"moduleDocs": [
|
|
10231
10231
|
{
|
|
10232
10232
|
"path": "agents/permissions.md",
|
|
10233
10233
|
"module": "permissions",
|
|
10234
|
-
"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\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- 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\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"
|
|
10234
|
+
"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\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"
|
|
10235
10235
|
},
|
|
10236
10236
|
{
|
|
10237
10237
|
"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-
|
|
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-BlDTirYc.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.
|
|
3
|
+
"version": "0.51.10",
|
|
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.11",
|
|
50
|
-
"@happyvertical/smrt-config": "0.51.
|
|
51
|
-
"@happyvertical/smrt-core": "0.51.
|
|
52
|
-
"@happyvertical/smrt-mobile-contract": "0.51.
|
|
53
|
-
"@happyvertical/smrt-profiles": "0.51.
|
|
54
|
-
"@happyvertical/smrt-tenancy": "0.51.
|
|
55
|
-
"@happyvertical/smrt-types": "0.51.
|
|
56
|
-
"@happyvertical/smrt-ui": "0.51.
|
|
50
|
+
"@happyvertical/smrt-config": "0.51.10",
|
|
51
|
+
"@happyvertical/smrt-core": "0.51.10",
|
|
52
|
+
"@happyvertical/smrt-mobile-contract": "0.51.10",
|
|
53
|
+
"@happyvertical/smrt-profiles": "0.51.10",
|
|
54
|
+
"@happyvertical/smrt-tenancy": "0.51.10",
|
|
55
|
+
"@happyvertical/smrt-types": "0.51.10",
|
|
56
|
+
"@happyvertical/smrt-ui": "0.51.10",
|
|
57
57
|
"@happyvertical/sql": "^0.89.11",
|
|
58
58
|
"jose": "^6.2.3"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@happyvertical/smrt-vitest": "0.51.
|
|
61
|
+
"@happyvertical/smrt-vitest": "0.51.10",
|
|
62
62
|
"@sveltejs/package": "^2.5.8",
|
|
63
63
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
64
64
|
"@types/node": "24.13.2",
|