@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.
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-users",
5
- "packageVersion": "0.51.9",
5
+ "packageVersion": "0.51.10",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-users:AccessRequestCollection": {
8
8
  "name": "accessrequestcollection",
@@ -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":"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"}
1
+ {"version":3,"file":"PermissionResolver.d.ts","sourceRoot":"","sources":["../../src/services/PermissionResolver.ts"],"names":[],"mappings":"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"}
@@ -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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"}
@@ -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.9",
6
+ "packageVersion": "0.51.10",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "642e2f59e52f82708229f2f3879388e55fbc1c9be530632e21153f758c0fdf06",
11
- "packageJson": "16f5baf5c4b083c5f4c14b789b3b18269c1dd53a1cb53fde6e01b479aa2c8dbd",
12
- "agents": "ec09e41d9c5e1b55abf454eb5b88a98637094e2aa67bffce2f11db0dcb68b441",
13
- "moduleDoc:agents/permissions.md": "f801411ae51fc625ad99054e284fd1abc6e7b2a5555dd8797a83dfd30273db68",
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-BfZmKdZW.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-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.9",
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.9",
51
- "@happyvertical/smrt-core": "0.51.9",
52
- "@happyvertical/smrt-mobile-contract": "0.51.9",
53
- "@happyvertical/smrt-profiles": "0.51.9",
54
- "@happyvertical/smrt-tenancy": "0.51.9",
55
- "@happyvertical/smrt-types": "0.51.9",
56
- "@happyvertical/smrt-ui": "0.51.9",
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.9",
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",