/** * Tenant hierarchy — what a role in a parent tenant is worth in its children. * * Multi-tenant products grow trees: a school group and its schools, a franchise * and its stores, an agency and its clients' workspaces. The membership model * stays flat (one row per user × tenant, the `TenantUser` shape the scopes * middleware reads), and the tree adds ONE rule on top: **a role held in a * parent is held in every descendant, unless the app says that role does not * travel.** A group's owner is the owner of each school without a membership row * in any of them; an explicit membership in a child always wins over the * inherited one; a nearer ancestor wins over a farther one. * * These are the rules, pure, over what the caller supplies: the memberships * (typically the packed `custom:tenantRoles` claim, see `unpackTenantRoles`) and * the tenant's ancestors or a membership tenant's descendants. The store that * answers those — `vk_tenants.parent_id` and `vk_tenant_closure`, or a projection's * own pair — is `./tree.ts`, whose `roleIn` / `reach` feed these two functions. * Two questions, two functions: * * - {@link inheritedTenantRole} — "may this caller act as tenant X, and as * what?" — for a request that names a tenant other than its own. * - {@link reachableTenants} — "which tenants may this caller act as at all?" * — for a session description or a tenant switcher. * * Which roles travel is the app's call ({@link HierarchyOptions.inherits}). The * default is that every role does; an app whose custom roles are rows of the * tenant that defined them (their scopes mean nothing in a child) passes a * predicate that keeps inheritance to its system roles. */ /** One membership: which role the caller holds in which tenant. Same shape as a packed claim entry. */ export interface TenantMembership { tenantId: string; role: string; } export interface HierarchyOptions { /** * Whether a role held in a parent counts in its descendants. Default: every * role does. Return `false` for roles whose meaning is bound to the tenant * that defined them (custom roles keyed by a per-tenant id, for instance). */ inherits?: (role: string, tenantId: string) => boolean; } /** How a caller comes to hold a role in a tenant. */ export interface HeldTenantRole { tenantId: string; role: string; /** The tenant the membership is actually in: the tenant itself, or the ancestor the role came from. */ via: string; /** Hops from `via` down to `tenantId`; `0` is the caller's own membership. */ depth: number; } /** * The role a caller holds in `tenantId`, and through which tenant — or `null` * when it has no business there. * * The caller's own membership in the tenant wins outright, whatever the role. * Otherwise `ancestors` is walked **nearest first** (the app reads them from * its tree: parent, grandparent, …) and the first ancestor the caller holds an * inheritable role in decides. Roles the app marks as non-inheriting are * skipped, not refused — a farther ancestor with a system role still counts. * * @example * ```ts * const pack = unpackTenantRoles(claims['custom:tenantRoles']); * const ancestors = await closure.ancestorsOf(requestedTenantId); // nearest first * const held = inheritedTenantRole(pack, requestedTenantId, ancestors, { * inherits: (role) => isSystemRole(role), * }); * if (!held) throw new ForbiddenError('This session may not act as that tenant'); * ``` */ export declare function inheritedTenantRole(memberships: ReadonlyMap | readonly TenantMembership[], tenantId: string, ancestors: readonly string[], options?: HierarchyOptions): HeldTenantRole | null; /** A descendant of a tenant, as the app's tree reports it. */ export interface TenantDescendant { tenantId: string; /** Hops down from the ancestor asked about: a child is `1`. */ depth: number; } /** * Every tenant a caller may act as: each membership's own tenant, plus — for a * membership whose role inherits — every descendant of that tenant, carrying the * role. A tenant reached more than once keeps the **nearest** claim to it: an * explicit membership (depth 0) over any inherited one, a parent's role over a * grandparent's. `descendantsOf` is asked once per membership whose role * inherits, so a caller with only leaf memberships costs no tree read at all. * * The result is unordered; the app joins names and sorts for display. */ export declare function reachableTenants(memberships: ReadonlyMap | readonly TenantMembership[], descendantsOf: (tenantId: string) => readonly TenantDescendant[], options?: HierarchyOptions): HeldTenantRole[]; //# sourceMappingURL=reach.d.ts.map