@vritti/api-sdk 0.4.2 → 0.4.3
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/dist/catalog-resolver.cjs +3 -3
- package/dist/catalog-resolver.cjs.map +1 -1
- package/dist/catalog-resolver.d.cts +2 -2
- package/dist/catalog-resolver.d.ts +2 -2
- package/dist/catalog-resolver.js +3 -3
- package/dist/catalog-resolver.js.map +1 -1
- package/dist/license.d.cts +1 -1
- package/dist/license.d.ts +1 -1
- package/dist/{types-Cj6uMN5E.d.cts → types-BQY0Aa1p.d.cts} +2 -2
- package/dist/{types-Cj6uMN5E.d.ts → types-BQY0Aa1p.d.ts} +2 -2
- package/package.json +1 -1
|
@@ -164,7 +164,7 @@ function snapshotFeatureKey(code, scope) {
|
|
|
164
164
|
return `${scope}.${code}`;
|
|
165
165
|
}
|
|
166
166
|
__name(snapshotFeatureKey, "snapshotFeatureKey");
|
|
167
|
-
var SNAPSHOT_SCHEMA_VERSION =
|
|
167
|
+
var SNAPSHOT_SCHEMA_VERSION = 6;
|
|
168
168
|
|
|
169
169
|
// src/catalog-resolver/catalog.builder.ts
|
|
170
170
|
function featureAppliesAtNode(applicableSiteTypes, siteType) {
|
|
@@ -187,7 +187,7 @@ function buildSiteCatalog(snapshot, businessCode, planCode, siteLocks, bucket, s
|
|
|
187
187
|
const locks = siteLocks;
|
|
188
188
|
const catalog = [];
|
|
189
189
|
const sortedApps = [
|
|
190
|
-
...
|
|
190
|
+
...snapshot.apps
|
|
191
191
|
].sort((a, b) => a.name.localeCompare(b.name));
|
|
192
192
|
for (const app of sortedApps) {
|
|
193
193
|
const businessAppFeatures = app.features.filter((ref) => scope === void 0 || ref.scope === scope).map((ref) => snapshot.features[snapshotFeatureKey(ref.code, ref.scope)]).filter((f) => !!f && // A UI bucket needs something to render, so a feature shipping no microfrontend is dropped.
|
|
@@ -506,7 +506,7 @@ function buildMatrix(snapshot, businessCode, planCode, siteLocks, allScopes, sit
|
|
|
506
506
|
locks
|
|
507
507
|
};
|
|
508
508
|
const apps = [];
|
|
509
|
-
for (const app of
|
|
509
|
+
for (const app of snapshot.apps) {
|
|
510
510
|
const counts = {
|
|
511
511
|
web: {
|
|
512
512
|
unlocked: 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/catalog-resolver/index.ts","../src/catalog-resolver/permission-deps.ts","../src/catalog-resolver/types.ts","../src/catalog-resolver/catalog.builder.ts","../src/catalog-resolver/compose-role-grants.ts","../src/catalog-resolver/resolve-user-features.ts","../src/catalog-resolver/site-matrix.builder.ts"],"sourcesContent":["// Catalog resolver — the single shared implementation of snapshot resolution (BU catalog, BU matrix, user features)\n\nexport {\n buildSiteCatalog,\n buildSiteRoles,\n featureAppliesAtNode,\n findFeatureByCode,\n isPlanMember,\n isSiteLockedOnPlatform,\n surfaceAllows,\n} from './catalog.builder';\nexport { type ComposeRoleGrantsParams, composeRoleGrants, type RevokedGrants } from './compose-role-grants';\nexport {\n buildDependsMap,\n cascadeLocked,\n type DependsMap,\n filterGrantedByDeps,\n prereqClosure,\n} from './permission-deps';\nexport {\n type ClientPlatform,\n type LockedPermission,\n type PermissionFeature,\n pickRouteForPlatform,\n type ResolveUserFeaturesParams,\n resolveUserFeatures,\n} from './resolve-user-features';\nexport {\n buildPlanMatrix,\n buildSiteMatrix,\n type SiteMatrix,\n type SiteMatrixApp,\n type SiteMatrixCell,\n type SiteMatrixFeature,\n type SiteMatrixPermission,\n} from './site-matrix.builder';\nexport {\n API_BUCKETS,\n API_SURFACES,\n type ApiBucket,\n type ApiSurface,\n BUCKET_BY_SURFACE,\n type BusinessVocabulary,\n type CatalogPermission,\n type FeatureCatalogEntry,\n type FeatureLocks,\n type FeatureUnlocks,\n isApiBucket,\n type LockReason,\n type PermissionGroupRef,\n PLATFORMS,\n type PlatformBucket,\n type PlatformCodes,\n type PlatformDenyCodes,\n type RoleItem,\n type ScopeType,\n SERVICE_CODES,\n type ServiceCode,\n SITE_TYPES,\n type SiteFeatureLocks,\n type SiteType,\n SNAPSHOT_SCHEMA_VERSION,\n type SnapshotApp,\n type SnapshotAppFeatureRef,\n type SnapshotBusiness,\n type SnapshotFeature,\n type SnapshotMicrofrontendMobile,\n type SnapshotMicrofrontends,\n type SnapshotMicrofrontendWeb,\n type SnapshotPermission,\n type SnapshotPlan,\n type SnapshotRoleTemplate,\n SURFACE_BY_BUCKET,\n snapshotFeatureKey,\n UI_PLATFORMS,\n type UiPlatformBucket,\n type VersionSnapshot,\n type VocabularyEntry,\n} from './types';\n","// Intra-feature permission prerequisites — only DIRECT edges are declared; the transitive closure is computed by recursion, cycle-guarded\n\nexport type DependsMap = Map<string, string[]>;\n\n// Builds a dependency map from a feature's permissions, keeping only edges to codes present in the set\nexport function buildDependsMap(permissions: Array<{ code: string; dependsOn?: string[] }>): DependsMap {\n const present = new Set(permissions.map((p) => p.code));\n const map: DependsMap = new Map();\n for (const p of permissions) {\n map.set(\n p.code,\n (p.dependsOn ?? []).filter((dep) => dep !== p.code && present.has(dep)),\n );\n }\n return map;\n}\n\n// Transitive prerequisite closure of a code (excludes the code itself), cycle-safe\nexport function prereqClosure(code: string, deps: DependsMap): string[] {\n const out = new Set<string>();\n const seen = new Set<string>([code]);\n const stack = [code];\n while (stack.length > 0) {\n const current = stack.pop() as string;\n for (const dep of deps.get(current) ?? []) {\n if (seen.has(dep)) continue;\n seen.add(dep);\n out.add(dep);\n stack.push(dep);\n }\n }\n return [...out];\n}\n\n// Codes locked after cascade: a code is locked if directly locked or any transitive prerequisite is (cycle-safe)\nexport function cascadeLocked(codes: string[], directlyLocked: Set<string>, deps: DependsMap): Set<string> {\n const locked = new Set<string>();\n const visiting = new Set<string>();\n const check = (code: string): boolean => {\n if (locked.has(code)) return true;\n if (directlyLocked.has(code)) {\n locked.add(code);\n return true;\n }\n if (visiting.has(code)) return false;\n visiting.add(code);\n const viaDep = (deps.get(code) ?? []).some(check);\n visiting.delete(code);\n if (viaDep) locked.add(code);\n return viaDep;\n };\n for (const code of codes) check(code);\n return locked;\n}\n\n// Keeps only codes whose FULL prerequisite closure is also present — drops a dependent missing any prerequisite (cycle-safe)\nexport function filterGrantedByDeps(granted: Set<string>, deps: DependsMap): Set<string> {\n const ok = new Set<string>();\n const visiting = new Set<string>();\n const check = (code: string): boolean => {\n if (ok.has(code)) return true;\n if (!granted.has(code)) return false;\n if (visiting.has(code)) return true;\n visiting.add(code);\n const satisfied = (deps.get(code) ?? []).every(check);\n visiting.delete(code);\n if (satisfied) ok.add(code);\n return satisfied;\n };\n for (const code of granted) check(code);\n return ok;\n}\n","// ——— Platform algebra — plan unlocks, role grants, and BU locks are all stored per platform bucket ———\n\n/**\n * The surfaces a permission can be granted on.\n *\n * `web` and `mobile` are UI buckets: a feature reaches them through a microfrontend, and a grant\n * there means a person can operate it on that surface. `graphql` and `http` are not UIs at all —\n * each is an API surface a credential signs its own requests against, so they have no\n * microfrontend and no route, and a feature needs neither to be reachable on one.\n *\n * Keeping the API buckets in the same algebra rather than beside it is what lets plan entitlement,\n * node feature locks and permission prerequisites bind an API client exactly as they bind a person.\n * One bucket per surface is what lets a plan entitle GraphQL and HTTP access independently.\n */\nexport type PlatformBucket = 'web' | 'mobile' | 'graphql' | 'http';\n\nexport const PLATFORMS: PlatformBucket[] = ['web', 'mobile', 'graphql', 'http'];\n\n/** Buckets that reach their feature through a microfrontend, and so require one to resolve. */\nexport type UiPlatformBucket = 'web' | 'mobile';\n\nexport const UI_PLATFORMS: UiPlatformBucket[] = ['web', 'mobile'];\n\n// The API surfaces an app credential can present — literally the values of core's `app_type` enum, so\n// enforcement is a plain lookup with no mapping. A feature declares which surfaces expose it.\nexport const API_SURFACES = ['GRAPHQL', 'HTTP'] as const;\nexport type ApiSurface = (typeof API_SURFACES)[number];\n\n/** Buckets that admit an API credential rather than a person — exactly one per surface. */\nexport type ApiBucket = Exclude<PlatformBucket, UiPlatformBucket>;\n\nexport const API_BUCKETS: ApiBucket[] = ['graphql', 'http'];\n\nexport const SURFACE_BY_BUCKET: Record<ApiBucket, ApiSurface> = { graphql: 'GRAPHQL', http: 'HTTP' };\nexport const BUCKET_BY_SURFACE: Record<ApiSurface, ApiBucket> = { GRAPHQL: 'graphql', HTTP: 'http' };\n\nexport function isApiBucket(bucket: PlatformBucket): bucket is ApiBucket {\n return bucket === 'graphql' || bucket === 'http';\n}\n\nexport interface PlatformCodes {\n web?: string[];\n mobile?: string[];\n graphql?: string[];\n http?: string[];\n}\n\nexport interface PlatformDenyCodes {\n web?: string[] | null;\n mobile?: string[] | null;\n graphql?: string[] | null;\n http?: string[] | null;\n}\n\nexport type FeatureUnlocks = Record<string, PlatformCodes>;\n\nexport type FeatureLocks = Record<string, PlatformDenyCodes>;\nexport type SiteFeatureLocks = FeatureLocks;\n\n// ——— Snapshot document shape — what gets stored in versions.snapshot and signed into the catalog license ———\n\nexport interface PermissionGroupRef {\n code: string;\n label: string;\n sortOrder: number;\n}\n\nexport interface SnapshotPermission {\n code: string;\n label: string;\n isGlobal: boolean;\n businesses: string[];\n dependsOn: string[];\n platforms: PlatformBucket[];\n // Code of the group this action sits under, resolved against the feature's `permissionGroups`.\n // Absent on a feature's own actions, which head the list under no heading.\n group?: string;\n}\nexport interface SnapshotMicrofrontendWeb {\n code: string;\n name: string;\n remoteEntry: string;\n exposedModule: string;\n routePrefix: string;\n}\nexport interface SnapshotMicrofrontendMobile {\n code: string;\n name: string;\n remoteEntryAndroid: string;\n remoteEntryIos: string;\n exposedModule: string;\n routePrefix: string;\n}\nexport interface SnapshotMicrofrontends {\n web?: SnapshotMicrofrontendWeb;\n mobile?: SnapshotMicrofrontendMobile;\n}\nexport type ScopeType = 'ORG' | 'LE' | 'SITE_GROUP' | 'SITE';\nexport type SiteType = 'OUTLET' | 'WAREHOUSE' | 'PRODUCTION';\nexport const SITE_TYPES: SiteType[] = ['OUTLET', 'WAREHOUSE', 'PRODUCTION'];\n// External services a feature can depend on — the org must have the service provisioned before the feature\n// unlocks. Add new services here and nowhere else in this package; every lock path is service-agnostic.\nexport const SERVICE_CODES = ['GITEA'] as const;\nexport type ServiceCode = (typeof SERVICE_CODES)[number];\nexport interface SnapshotFeature {\n code: string;\n name: string;\n lucideIcon: string;\n sfSymbol: string;\n materialSymbol: string;\n scope: ScopeType;\n applicableSiteTypes: SiteType[];\n permissions: SnapshotPermission[];\n microfrontends: SnapshotMicrofrontends;\n requiredServices: ServiceCode[];\n // The feature's sub-resources, carried once rather than repeated on each of their permissions\n permissionGroups: PermissionGroupRef[];\n // Strict — it decides which of the `graphql`/`http` buckets the feature offers at all, and `[]` offers neither\n apiSurfaces: ApiSurface[];\n}\nexport interface SnapshotAppFeatureRef {\n code: string;\n scope: ScopeType;\n}\nexport interface SnapshotApp {\n code: string;\n name: string;\n icon: string;\n sortOrder: number;\n features: SnapshotAppFeatureRef[];\n}\nexport interface SnapshotRoleTemplate {\n name: string;\n code: string;\n scope: ScopeType;\n siteType: SiteType;\n features: FeatureUnlocks;\n}\nexport interface SnapshotPlan {\n code: string;\n name: string;\n isCustom: boolean;\n maxSites: number | null;\n unlockedPermissions: FeatureUnlocks;\n}\nexport interface VocabularyEntry {\n singular: string;\n plural: string;\n}\nexport interface BusinessVocabulary {\n site?: VocabularyEntry;\n siteGroup?: VocabularyEntry;\n outlet?: VocabularyEntry;\n warehouse?: VocabularyEntry;\n production?: VocabularyEntry;\n}\nexport interface SnapshotBusiness {\n name: string;\n vocabulary?: BusinessVocabulary;\n apps: SnapshotApp[];\n roleTemplates: Record<string, SnapshotRoleTemplate>;\n plans: Record<string, SnapshotPlan>;\n}\nexport interface VersionSnapshot {\n schemaVersion?: number;\n // Flat feature dictionary keyed by `${scope}.${code}` (see snapshotFeatureKey) — same-code features at different scopes stay distinct\n features: Record<string, SnapshotFeature>;\n businesses: Record<string, SnapshotBusiness>;\n}\n\n// Composite key for the snapshot feature dictionary — feature identity is (scope, code)\nexport function snapshotFeatureKey(code: string, scope: ScopeType): string {\n return `${scope}.${code}`;\n}\n\nexport const SNAPSHOT_SCHEMA_VERSION = 5;\n\n// SERVICE = the org has not provisioned an external service the feature declares; the specific services are\n// reported alongside in `missingServices` so callers never branch on a service code baked into this union\nexport type LockReason = 'PLAN' | 'SITE' | 'SERVICE';\n\nexport interface CatalogPermission {\n code: string;\n locked: boolean;\n lockReason: LockReason | null;\n unlockPlans: string[];\n missingServices: ServiceCode[];\n}\n\nexport interface FeatureCatalogEntry {\n code: string;\n name: string;\n lucideIcon: string | null;\n sfSymbol: string;\n materialSymbol: string;\n web: {\n remoteEntry: string;\n exposedModule: string;\n routePrefix: string;\n } | null;\n mobile: {\n remoteEntryAndroid: string;\n remoteEntryIos: string;\n exposedModule: string;\n routePrefix: string;\n } | null;\n appCode: string;\n appName: string;\n appIcon: string | null;\n appSortOrder: number;\n locked: boolean;\n lockReason: LockReason | null;\n unlockPlans: string[];\n missingServices: ServiceCode[];\n permissions: CatalogPermission[];\n}\n\nexport type RoleItem = SnapshotRoleTemplate;\n","import { buildDependsMap, cascadeLocked, prereqClosure } from './permission-deps';\nimport type {\n ApiSurface,\n CatalogPermission,\n FeatureCatalogEntry,\n LockReason,\n PlatformBucket,\n PlatformCodes,\n RoleItem,\n ScopeType,\n ServiceCode,\n SiteFeatureLocks,\n SiteType,\n SnapshotFeature,\n SnapshotPlan,\n VersionSnapshot,\n} from './types';\nimport { isApiBucket, PLATFORMS, SURFACE_BY_BUCKET, snapshotFeatureKey } from './types';\n\n// Whether a feature with the given site-type applicability is exposed at this site type\nexport function featureAppliesAtNode(applicableSiteTypes: SiteType[], siteType: SiteType): boolean {\n return applicableSiteTypes.includes(siteType);\n}\n\n// Scope-agnostic lookup of a feature by bare code — grants/locks key features by code alone, so the first scope-variant's shared metadata (permission graph) answers\nexport function findFeatureByCode(snapshot: VersionSnapshot, code: string): SnapshotFeature | undefined {\n for (const feature of Object.values(snapshot.features)) {\n if (feature.code === code) return feature;\n }\n return undefined;\n}\n\n// Builds the per-site catalog for ONE platform bucket — plan is the ceiling, siteLocks is a deny-list within it; each permission carries locked + lockReason + unlockPlans\n// availableServices defaults to none, so a caller that doesn't know the org's provisioned services locks every service-dependent feature rather than leaking it\nexport function buildSiteCatalog(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n siteLocks: SiteFeatureLocks | undefined,\n bucket: PlatformBucket,\n siteType?: SiteType,\n scope?: ScopeType,\n availableServices: ServiceCode[] = [],\n): FeatureCatalogEntry[] {\n if (!businessCode) return [];\n const business = snapshot.businesses[businessCode];\n if (!business) return [];\n const plans = business.plans;\n const plan = planCode ? plans[planCode] : undefined;\n const locks = siteLocks;\n\n const catalog: FeatureCatalogEntry[] = [];\n // Iterate apps alphabetically by name so the resolved feature list (→ core-web sidebar) is app-alphabetical without any frontend re-sort\n const sortedApps = [...business.apps].sort((a, b) => a.name.localeCompare(b.name));\n for (const app of sortedApps) {\n // The app's renderable features (each ref pins scope+code to one app), dropped when they don't belong to this workspace scope or node type (outlet vs container)\n const businessAppFeatures = app.features\n .filter((ref) => scope === undefined || ref.scope === scope)\n .map((ref) => snapshot.features[snapshotFeatureKey(ref.code, ref.scope)])\n .filter(\n (f): f is SnapshotFeature =>\n !!f &&\n // A UI bucket needs something to render, so a feature shipping no microfrontend is dropped.\n // An API bucket renders nothing — there a feature is admitted by the surfaces it declares\n // instead, so a GRAPHQL credential never resolves an HTTP-only feature. A surface-excluded\n // feature vanishes from the catalog entirely, which is what makes resolution fail closed.\n (isApiBucket(bucket)\n ? surfaceAllows(f.apiSurfaces, SURFACE_BY_BUCKET[bucket])\n : !!(f.microfrontends?.web || f.microfrontends?.mobile)) &&\n (siteType === undefined || featureAppliesAtNode(f.applicableSiteTypes, siteType)),\n );\n\n if (businessAppFeatures.length === 0) continue;\n\n // Emit EVERY business feature so a role's grant on a plan-omitted feature still resolves as a locked tile instead of vanishing\n for (const feature of businessAppFeatures) {\n const membership = plan?.unlockedPermissions[feature.code];\n // Routes are exposed wherever the feature SHIPS — membership never hides them\n const web = feature.microfrontends?.web;\n const mobile = feature.microfrontends?.mobile;\n\n // Feature-level lock is EXPLICIT: plan must include the feature on this bucket, the site must not null-lock\n // the platform, and every external service the feature declares must be provisioned for the org\n const memberOnBucket = membership?.[bucket] !== undefined;\n const sitePlatformLocked = locks?.[feature.code]?.[bucket] === null;\n const missingServices = unmetServices(feature, availableServices);\n // Unmet services lock every permission too — otherwise the feature reads locked while its actions still\n // report as available, which is not how plan and site locks behave\n const permissions = buildPermissions(feature, businessCode, membership, locks, plans, bucket, missingServices);\n const lockReason = resolveLockReason(!memberOnBucket, sitePlatformLocked, missingServices);\n const locked = lockReason !== null;\n const unlockPlans = lockReason === 'PLAN' ? plansIncludingFeature(plans, feature.code, bucket) : [];\n\n catalog.push({\n code: feature.code,\n name: feature.name,\n lucideIcon: feature.lucideIcon ?? null,\n sfSymbol: feature.sfSymbol ?? 'square',\n materialSymbol: feature.materialSymbol ?? 'square',\n web: web\n ? {\n remoteEntry: web.remoteEntry ?? '',\n exposedModule: web.exposedModule ?? '',\n routePrefix: web.routePrefix ?? '',\n }\n : null,\n mobile: mobile\n ? {\n remoteEntryAndroid: mobile.remoteEntryAndroid ?? '',\n remoteEntryIos: mobile.remoteEntryIos ?? '',\n exposedModule: mobile.exposedModule ?? '',\n routePrefix: mobile.routePrefix ?? '',\n }\n : null,\n appCode: app.code,\n appName: app.name,\n appIcon: app.icon ?? null,\n appSortOrder: app.sortOrder ?? 0,\n locked,\n lockReason,\n unlockPlans,\n missingServices,\n permissions,\n });\n }\n }\n return catalog;\n}\n\n// A feature is a plan member when its unlock entry exists on at least one platform (even with zero actions)\nexport function isPlanMember(entry: PlatformCodes | undefined): boolean {\n if (!entry) return false;\n return PLATFORMS.some((platform) => entry[platform] !== undefined);\n}\n\n/**\n * Whether a feature's declared API surfaces admit a caller's surface.\n *\n * Lenient only about the caller: resolving without a surface (cloud's matrix builders, UI buckets)\n * filters nothing. The declared list is always strict — including `[]`, which admits no surface.\n */\nexport function surfaceAllows(surfaces: ApiSurface[], surface: ApiSurface | undefined): boolean {\n return surface === undefined || surfaces.includes(surface);\n}\n\n// The one place lock precedence is decided, for features and permissions alike; null means nothing locks.\n// Plan is the ceiling (an unentitled feature must upsell, not send the user to provision something they still\n// couldn't use), then the site deny-list, then any unprovisioned service.\nfunction resolveLockReason(\n planLocked: boolean,\n siteLocked: boolean,\n missingServices: ServiceCode[],\n): LockReason | null {\n if (planLocked) return 'PLAN';\n if (siteLocked) return 'SITE';\n if (missingServices.length > 0) return 'SERVICE';\n return null;\n}\n\n// The services a feature declares that this org has not provisioned\nfunction unmetServices(feature: SnapshotFeature, availableServices: ServiceCode[]): ServiceCode[] {\n return feature.requiredServices.filter((service) => !availableServices.includes(service));\n}\n\n// Per-platform site-lock primitive: null locks the whole feature, string[] locks those codes, absent = not locked\nexport function isSiteLockedOnPlatform(\n entry: SiteFeatureLocks[string] | undefined,\n platform: PlatformBucket,\n code: string,\n): boolean {\n const locks = entry?.[platform];\n return locks === null || (locks?.includes(code) ?? false);\n}\n\n// A feature's business-scoped permissions, each tagged with locked + reason against the plan and site deny-list\n// (bucket-scoped). Unmet services lock the whole set — an unprovisioned service blocks every action on the feature.\nfunction buildPermissions(\n feature: SnapshotFeature,\n businessCode: string,\n planMembership: PlatformCodes | undefined,\n siteLocks: SiteFeatureLocks | undefined,\n plans: Record<string, SnapshotPlan>,\n bucket: PlatformBucket,\n missingServices: ServiceCode[] = [],\n): CatalogPermission[] {\n const planUnlocked = new Set(planMembership?.[bucket] ?? []);\n const lockEntry = siteLocks?.[feature.code];\n\n // Two filters, and the second is the point: a feature reaching this surface does not mean every\n // action under it does. A code omits the bucket when no route there enforces it, so offering it\n // would promise a capability nothing can check.\n const perms = feature.permissions\n .filter((p) => p.isGlobal || p.businesses.includes(businessCode))\n .filter((p) => p.platforms.includes(bucket));\n const deps = buildDependsMap(perms);\n const codes = perms.map((p) => p.code);\n\n // Direct plan/site locks, then cascade so a locked prerequisite (e.g. view) locks its dependents (add/edit/delete)\n const directlyPlanLocked = new Set<string>();\n const directlySiteLocked = new Set<string>();\n for (const p of perms) {\n if (!planUnlocked.has(p.code)) directlyPlanLocked.add(p.code);\n if (isSiteLockedOnPlatform(lockEntry, bucket, p.code)) directlySiteLocked.add(p.code);\n }\n const directlyLocked = new Set<string>([...directlyPlanLocked, ...directlySiteLocked]);\n const lockedSet = cascadeLocked(codes, directlyLocked, deps);\n\n return perms.map((p) => {\n // A permission is enabled only if it AND its whole prerequisite closure are unlocked — reason/upsell reflect that\n const closure = [p.code, ...prereqClosure(p.code, deps)];\n const cascaded = lockedSet.has(p.code);\n const planReason = cascaded && closure.some((c) => directlyPlanLocked.has(c));\n const siteReason = cascaded && closure.some((c) => directlySiteLocked.has(c));\n const lockReason = resolveLockReason(planReason, siteReason, missingServices);\n const locked = lockReason !== null;\n const unlockPlans = lockReason === 'PLAN' ? plansUnlockingClosure(plans, feature.code, closure, bucket) : [];\n return { code: p.code, locked, lockReason, unlockPlans, missingServices };\n });\n}\n\n// Plan codes (in the business) whose unlocked set includes the permission AND its whole prerequisite closure — upsell targets\nfunction plansUnlockingClosure(\n plans: Record<string, SnapshotPlan>,\n featureCode: string,\n closure: string[],\n bucket: PlatformBucket,\n): string[] {\n const result: string[] = [];\n for (const [code, plan] of Object.entries(plans)) {\n const unlocked = plan.unlockedPermissions[featureCode]?.[bucket];\n if (unlocked && closure.every((c) => unlocked.includes(c))) result.push(code);\n }\n return result;\n}\n\n// Plan codes (in the business) that include this feature on the bucket — the feature-level upsell targets\nfunction plansIncludingFeature(\n plans: Record<string, SnapshotPlan>,\n featureCode: string,\n bucket: PlatformBucket,\n): string[] {\n const result: string[] = [];\n for (const [code, plan] of Object.entries(plans)) {\n if (plan.unlockedPermissions[featureCode]?.[bucket] !== undefined) result.push(code);\n }\n return result;\n}\n\n// The business's role templates as provisionable role items for core (identical shapes)\nexport function buildSiteRoles(snapshot: VersionSnapshot, businessCode: string | undefined): RoleItem[] {\n if (!businessCode) return [];\n const business = snapshot.businesses[businessCode];\n if (!business) return [];\n return Object.values(business.roleTemplates);\n}\n","import { type FeatureUnlocks, PLATFORMS, type PlatformCodes, type PlatformDenyCodes } from './types';\n\nexport type RevokedGrants = Record<string, PlatformDenyCodes>;\n\nexport interface ComposeRoleGrantsParams {\n baseFeatures: FeatureUnlocks | undefined;\n additions: FeatureUnlocks;\n revoked: RevokedGrants | undefined;\n}\n\n// Deduped union of two optional code lists — undefined on both sides means no platform membership\nfunction unionBucket(base: string[] | undefined, add: string[] | undefined): string[] | undefined {\n if (base === undefined && add === undefined) return undefined;\n return [...new Set([...(base ?? []), ...(add ?? [])])];\n}\n\n// Composes a custom role's effective grants: merge(base ∪ additions) − revoked (design doc §10); inputs are never mutated\nexport function composeRoleGrants(params: ComposeRoleGrantsParams): FeatureUnlocks {\n const { baseFeatures, additions, revoked } = params;\n\n const result: FeatureUnlocks = {};\n const featureCodes = new Set([...Object.keys(baseFeatures ?? {}), ...Object.keys(additions)]);\n\n for (const code of featureCodes) {\n const base = baseFeatures?.[code] ?? {};\n const add = additions[code] ?? {};\n const revokes = revoked?.[code];\n\n const composed: PlatformCodes = {};\n for (const bucket of PLATFORMS) {\n const merged = unionBucket(base[bucket], add[bucket]);\n if (merged === undefined) continue;\n const revoke = revokes?.[bucket];\n // null revokes the whole platform (membership + all codes); string[] subtracts codes but keeps membership\n if (revoke === null) continue;\n composed[bucket] = revoke === undefined ? merged : merged.filter((c) => !revoke.includes(c));\n }\n\n // A feature with no surviving platform membership disappears from the effective set.\n // Iterates PLATFORMS rather than naming buckets — the web/mobile-only version silently\n // dropped a grant surviving only on an API bucket.\n if (PLATFORMS.every((bucket) => composed[bucket] === undefined)) continue;\n result[code] = composed;\n }\n\n return result;\n}\n","import { buildSiteCatalog, findFeatureByCode } from './catalog.builder';\nimport { buildDependsMap, filterGrantedByDeps } from './permission-deps';\nimport type {\n FeatureUnlocks,\n LockReason,\n PlatformBucket,\n ScopeType,\n ServiceCode,\n SiteFeatureLocks,\n SiteType,\n VersionSnapshot,\n} from './types';\nimport { isApiBucket, snapshotFeatureKey } from './types';\n\n/**\n * The caller's surface, as the caller reports it.\n *\n * Finer than `PlatformBucket` on the mobile side — `ios` and `android` load different remote\n * entries but share one grant bucket. The API platforms are one-to-one with their buckets: an\n * API client has no variants because it has no UI.\n */\nexport type ClientPlatform = 'web' | 'ios' | 'android' | 'graphql' | 'http';\n\n// Exhaustive by type, so adding a ClientPlatform without deciding its bucket fails the build instead\n// of silently falling through to mobile — which is how an API caller would end up resolving a UI bucket.\nconst BUCKET_BY_CLIENT: Record<ClientPlatform, PlatformBucket> = {\n web: 'web',\n ios: 'mobile',\n android: 'mobile',\n graphql: 'graphql',\n http: 'http',\n};\n\n/**\n * Stands in for the microfrontend an API client does not load.\n *\n * `PermissionFeature.route` is non-optional and read by the web sidebar and the mobile host to\n * mount a remote. Nothing on the API paths reads it — the permission interceptor uses `code`,\n * `permissions` and `locked` — so an empty route keeps one shape for every bucket instead of\n * widening the field to null across every consumer.\n */\nconst EMPTY_ROUTE = { remoteEntry: '', exposedModule: '', routePrefix: '' };\n\nexport interface LockedPermission {\n code: string;\n reason: LockReason | null;\n unlockPlans: string[];\n missingServices: ServiceCode[];\n}\n\nexport interface PlanUpsell {\n plan: string;\n features: string[];\n}\n\nexport interface PermissionFeature {\n code: string;\n name: string;\n lucideIcon: string | null;\n sfSymbol: string;\n materialSymbol: string;\n permissions: string[];\n locked: boolean;\n lockReason: LockReason | null;\n unlockPlans: string[];\n // Which declared services the org has not provisioned — empty unless lockReason is 'SERVICE'\n missingServices: ServiceCode[];\n lockedPermissions: LockedPermission[];\n upsell: PlanUpsell[];\n route: {\n remoteEntry: string;\n exposedModule: string;\n routePrefix: string;\n };\n appCode: string;\n appName: string;\n appIcon: string | null;\n appSortOrder: number;\n}\n\nexport interface ResolveUserFeaturesParams {\n snapshot: VersionSnapshot;\n businessCode: string;\n planCode: string | undefined;\n siteLocks: SiteFeatureLocks | undefined;\n roleFeatures: FeatureUnlocks;\n platform: ClientPlatform;\n siteType?: SiteType;\n scope?: ScopeType;\n // External services the org has provisioned; omitting it locks every service-dependent feature\n availableServices?: ServiceCode[];\n}\n\n// Resolves the features + MF config a user sees at a BU: plan ∧ BU catalog intersected with the role's grants, filtered to the requested platform\nexport function resolveUserFeatures(params: ResolveUserFeaturesParams): PermissionFeature[] {\n const { snapshot, businessCode, planCode, siteLocks, platform, siteType, scope, availableServices } = params;\n\n // Plan unlocks, BU locks, and role grants are stored per platform; resolve only the requesting\n // surface's bucket (web → web; ios/android → mobile; graphql/http → themselves)\n const bucket: PlatformBucket = BUCKET_BY_CLIENT[platform];\n\n const roleFeatures = params.roleFeatures;\n\n // Grants/plans/locks key features by bare code; resolve to the workspace scope's variant (or any variant when unscoped)\n const featureByCode = (code: string) =>\n scope ? snapshot.features[snapshotFeatureKey(code, scope)] : findFeatureByCode(snapshot, code);\n\n // Plan ∧ BU overlay for this bucket, filtered to features that apply to this workspace scope and node type — emits EVERY applicable business feature (plan non-members come out fully locked)\n const catalog = buildSiteCatalog(\n snapshot,\n businessCode,\n planCode,\n siteLocks,\n bucket,\n siteType,\n scope,\n availableServices,\n );\n const catalogMap = new Map(catalog.map((f) => [f.code, f]));\n\n // Per-plan feature-name delta vs the current plan — feeds the plan-locked upsell screen\n const businessPlans = snapshot.businesses[businessCode]?.plans ?? {};\n const currentUnlockedCodes = new Set<string>();\n if (planCode && businessPlans[planCode]) {\n for (const [featureCode, platforms] of Object.entries(businessPlans[planCode].unlockedPermissions)) {\n if (platforms[bucket] !== undefined) currentUnlockedCodes.add(featureCode);\n }\n }\n const planAdds = new Map<string, Array<{ code: string; name: string }>>();\n for (const [planKey, plan] of Object.entries(businessPlans)) {\n if (planKey === planCode) continue;\n const adds: Array<{ code: string; name: string }> = [];\n for (const [featureCode, platforms] of Object.entries(plan.unlockedPermissions)) {\n if (platforms[bucket] === undefined || currentUnlockedCodes.has(featureCode)) continue;\n const name = featureByCode(featureCode)?.name;\n if (name) adds.push({ code: featureCode, name });\n }\n planAdds.set(planKey, adds);\n }\n\n // Granted permission set per feature, taking only this platform's grants\n const grantedFeatures = new Map<string, Set<string>>();\n for (const [code, grant] of Object.entries(roleFeatures)) {\n // Membership is the gate: undefined = not a member on this platform; [] = member with no actions (view-only)\n const granted = grant[bucket];\n if (granted === undefined) continue;\n if (!grantedFeatures.has(code)) grantedFeatures.set(code, new Set());\n for (const perm of granted) grantedFeatures.get(code)?.add(perm);\n }\n\n // Cross-reference the granted features with the catalog to build the response\n const features: PermissionFeature[] = [];\n for (const [code, permsSet] of grantedFeatures) {\n const catalogEntry = catalogMap.get(code);\n if (!catalogEntry) continue;\n\n // A UI bucket reaches its feature by loading a microfrontend, so a feature not published to\n // this platform is omitted rather than handed over as an unloadable tile. An API client loads\n // nothing — requiring a route there would make every headless feature permanently ungrantable.\n const route = isApiBucket(bucket) ? EMPTY_ROUTE : pickRouteForPlatform(catalogEntry, platform);\n if (!route) continue;\n\n // Drop granted permissions whose intra-feature prerequisites aren't also granted (e.g. add needs view)\n const featureDeps = buildDependsMap(featureByCode(code)?.permissions ?? []);\n // Plan/BU lock a subset of permissions; surface which GRANTED ones are locked + why + how to unlock (upsell)\n const permByCode = new Map(catalogEntry.permissions.map((p) => [p.code, p]));\n // Intersected with the catalog, which now omits codes this surface does not implement. Without\n // this a grant made before the flags existed — or written straight through the API — would keep\n // resolving on a bucket where no route enforces it. The picker filtering alone is cosmetic; this\n // is what makes an unimplemented grant genuinely inert.\n const grantedPerms = [...filterGrantedByDeps(permsSet, featureDeps)].filter((c) => permByCode.has(c));\n const lockedPermissions: LockedPermission[] = grantedPerms\n .map((c) => permByCode.get(c))\n .filter((p): p is NonNullable<typeof p> => !!p?.locked)\n .map((p) => ({\n code: p.code,\n reason: p.lockReason ?? null,\n unlockPlans: p.unlockPlans,\n missingServices: p.missingServices,\n }));\n\n // For a plan-locked feature, list the extra features each unlocking plan would add (excluding this feature)\n const upsell: PlanUpsell[] =\n catalogEntry.locked && catalogEntry.lockReason === 'PLAN'\n ? catalogEntry.unlockPlans\n .map((plan) => ({\n plan,\n features: (planAdds.get(plan) ?? []).filter((f) => f.code !== code).map((f) => f.name),\n }))\n .filter((group) => group.features.length > 0)\n : [];\n\n features.push({\n code,\n name: catalogEntry.name,\n lucideIcon: catalogEntry.lucideIcon,\n sfSymbol: catalogEntry.sfSymbol,\n materialSymbol: catalogEntry.materialSymbol,\n permissions: grantedPerms,\n locked: catalogEntry.locked ?? false,\n lockReason: catalogEntry.lockReason ?? null,\n unlockPlans: catalogEntry.unlockPlans,\n missingServices: catalogEntry.missingServices,\n lockedPermissions,\n upsell,\n route,\n appCode: catalogEntry.appCode,\n appName: catalogEntry.appName,\n appIcon: catalogEntry.appIcon,\n appSortOrder: catalogEntry.appSortOrder,\n });\n }\n\n // Order app-alphabetically so the core-web sidebar (groups by app) renders apps sorted without any frontend re-sort;\n // stable sort keeps each app's features in their existing relative order\n features.sort((a, b) => a.appName.localeCompare(b.appName));\n\n return features;\n}\n\n// Selects the route block from a catalog entry for the requested platform, or null when it doesn't publish there\nexport function pickRouteForPlatform(\n entry: {\n web: {\n remoteEntry: string;\n exposedModule: string;\n routePrefix: string;\n } | null;\n mobile: {\n remoteEntryAndroid: string;\n remoteEntryIos: string;\n exposedModule: string;\n routePrefix: string;\n } | null;\n },\n platform: ClientPlatform,\n): { remoteEntry: string; exposedModule: string; routePrefix: string } | null {\n // API platforms load nothing — resolveUserFeatures never routes them here, and answering with the\n // web block for an unhandled value would hand an API caller a remote it cannot mount\n if (platform === 'graphql' || platform === 'http') return null;\n if (platform === 'ios' || platform === 'android') {\n if (!entry.mobile) return null;\n return {\n remoteEntry: platform === 'ios' ? entry.mobile.remoteEntryIos : entry.mobile.remoteEntryAndroid,\n exposedModule: entry.mobile.exposedModule,\n routePrefix: entry.mobile.routePrefix,\n };\n }\n // Web\n if (!entry.web) return null;\n return {\n remoteEntry: entry.web.remoteEntry,\n exposedModule: entry.web.exposedModule,\n routePrefix: entry.web.routePrefix,\n };\n}\n","import { featureAppliesAtNode, isPlanMember, isSiteLockedOnPlatform } from './catalog.builder';\nimport {\n API_BUCKETS,\n type ApiSurface,\n type PlatformBucket,\n type ScopeType,\n type SiteFeatureLocks,\n type SiteType,\n type SnapshotPlan,\n SURFACE_BY_BUCKET,\n snapshotFeatureKey,\n UI_PLATFORMS,\n type VersionSnapshot,\n} from './types';\n\nexport interface SiteMatrixCell {\n inPlan: boolean;\n selected: boolean;\n availableIn: string[];\n}\n\nexport interface SiteMatrixPermission {\n code: string;\n label: string;\n dependsOn: string[];\n web: SiteMatrixCell | null;\n mobile: SiteMatrixCell | null;\n graphql: SiteMatrixCell | null;\n http: SiteMatrixCell | null;\n}\n\nexport interface SiteMatrixFeature {\n code: string;\n name: string;\n icon: string | null;\n scope: ScopeType;\n applicableSiteTypes: SiteType[];\n platforms: PlatformBucket[];\n inPlan: boolean;\n availableIn: string[];\n // The API surfaces the feature declares — what lets the app-credential editor filter by the\n // credential's type.\n apiSurfaces: ApiSurface[];\n permissions: SiteMatrixPermission[];\n}\n\nexport interface MatrixCounts {\n unlocked: number;\n total: number;\n}\n\nexport interface SiteMatrixApp {\n code: string;\n name: string;\n icon: string | null;\n // Counted per surface, not as one total: a consumer showing a subset of the columns (the\n // app-credential editor shows exactly one) sums the surfaces it renders. One number covering all\n // four read as \"20/20 unlocked\" above the 5 checkboxes actually on screen.\n counts: Record<PlatformBucket, MatrixCounts>;\n features: SiteMatrixFeature[];\n}\n\nexport interface SiteMatrix {\n plan: { code: string; name: string };\n apps: SiteMatrixApp[];\n locks: SiteFeatureLocks;\n}\n\n// Builds the SITE-only apps/features/permissions matrix — not filtered to plan members; plan-locked items carry inPlan=false + availableIn\nexport function buildSiteMatrix(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n siteLocks: SiteFeatureLocks | undefined,\n siteType?: SiteType,\n): SiteMatrix {\n return buildMatrix(snapshot, businessCode, planCode, siteLocks, false, siteType);\n}\n\n// Builds the all-scopes apps/features/permissions matrix — every scope's features included, each carrying its real scope; powers the Plan Overview + Create Custom Role picker\nexport function buildPlanMatrix(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n siteLocks?: SiteFeatureLocks,\n): SiteMatrix {\n return buildMatrix(snapshot, businessCode, planCode, siteLocks, true);\n}\n\n// Shared matrix builder — allScopes=false keeps only SITE refs; allScopes=true includes every scope and emits each feature's real scope\nfunction buildMatrix(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n siteLocks: SiteFeatureLocks | undefined,\n allScopes: boolean,\n siteType?: SiteType,\n): SiteMatrix {\n const business = businessCode ? snapshot.businesses[businessCode] : undefined;\n const plans = business?.plans ?? {};\n const plan = planCode ? plans[planCode] : undefined;\n const planMeta = { code: planCode ?? '', name: plan?.name ?? planCode ?? '' };\n const locks = siteLocks ?? {};\n if (!business || !plan) return { plan: planMeta, apps: [], locks };\n\n const apps: SiteMatrixApp[] = [];\n for (const app of business.apps) {\n const counts: Record<PlatformBucket, MatrixCounts> = {\n web: { unlocked: 0, total: 0 },\n mobile: { unlocked: 0, total: 0 },\n graphql: { unlocked: 0, total: 0 },\n http: { unlocked: 0, total: 0 },\n };\n const features: SiteMatrixFeature[] = [];\n\n for (const ref of app.features) {\n if (!allScopes && ref.scope !== 'SITE') continue;\n const code = ref.code;\n const feature = snapshot.features[snapshotFeatureKey(code, ref.scope)];\n if (!feature) continue;\n if (siteType !== undefined && !featureAppliesAtNode(feature.applicableSiteTypes, siteType)) continue;\n // A UI bucket is offered only where the feature publishes a microfrontend; each API bucket is\n // offered where the feature declares its surface. An undeclared surface shows an em dash like\n // a missing microfrontend does.\n const platforms: PlatformBucket[] = [\n ...UI_PLATFORMS.filter((p) => !!feature.microfrontends?.[p]),\n ...API_BUCKETS.filter((b) => feature.apiSurfaces.includes(SURFACE_BY_BUCKET[b])),\n ];\n\n const groupByCode = new Map(feature.permissionGroups.map((g) => [g.code, g]));\n const membership = plan.unlockedPermissions[code];\n const featureInPlan = isPlanMember(membership);\n const siteEntry = siteLocks?.[code];\n\n const permissions: SiteMatrixPermission[] = feature.permissions\n .filter((p) => p.isGlobal || p.businesses.includes(businessCode ?? ''))\n .map((p) => {\n const cell = (plat: PlatformBucket): SiteMatrixCell | null => {\n // The feature must reach this bucket AND this code must be implemented on it — the same\n // two gates buildSiteCatalog applies, so the matrix and the catalog cannot disagree\n if (!platforms.includes(plat) || !p.platforms.includes(plat)) return null;\n const planCodes = membership?.[plat];\n const inPlan = featureInPlan && planCodes !== undefined && planCodes.includes(p.code);\n // Deny-list: an in-plan cell is selected unless the site locks it on this platform\n const selected = inPlan && !isSiteLockedOnPlatform(siteEntry, plat, p.code);\n const availableIn = inPlan ? [] : plansUnlockingPerm(plans, code, p.code, plat, planCode);\n counts[plat].total += 1;\n if (inPlan) counts[plat].unlocked += 1;\n return { inPlan, selected, availableIn };\n };\n return {\n code: p.code,\n label: p.label,\n dependsOn: p.dependsOn,\n group: p.group ? groupByCode.get(p.group) : undefined,\n web: cell('web'),\n mobile: cell('mobile'),\n graphql: cell('graphql'),\n http: cell('http'),\n };\n });\n\n features.push({\n code: feature.code,\n name: feature.name,\n icon: feature.lucideIcon ?? null,\n scope: feature.scope,\n applicableSiteTypes: feature.applicableSiteTypes,\n platforms,\n inPlan: featureInPlan,\n availableIn: featureInPlan ? [] : plansIncludingFeature(plans, code, planCode),\n apiSurfaces: feature.apiSurfaces,\n permissions,\n });\n }\n\n if (features.length === 0) continue;\n apps.push({ code: app.code, name: app.name, icon: app.icon ?? null, counts, features });\n }\n\n // Emit apps alphabetically by name so every consumer (Plan Overview, Role picker, all Locks screens) renders them sorted\n apps.sort((a, b) => a.name.localeCompare(b.name));\n\n return { plan: planMeta, apps, locks };\n}\n\n// Names of other plans (excluding the org's own) that unlock this feature+permission on the given platform\nfunction plansUnlockingPerm(\n plans: Record<string, SnapshotPlan>,\n featureCode: string,\n permCode: string,\n platform: PlatformBucket,\n excludeCode: string | undefined,\n): string[] {\n const names: string[] = [];\n for (const [code, p] of Object.entries(plans)) {\n if (code === excludeCode) continue;\n if ((p.unlockedPermissions[featureCode]?.[platform] ?? []).includes(permCode)) names.push(p.name);\n }\n return names;\n}\n\n// Names of other plans (excluding the org's own) that include this feature at all (membership) — feature-level upsell\nfunction plansIncludingFeature(\n plans: Record<string, SnapshotPlan>,\n featureCode: string,\n excludeCode: string | undefined,\n): string[] {\n const names: string[] = [];\n for (const [code, p] of Object.entries(plans)) {\n if (code === excludeCode) continue;\n if (isPlanMember(p.unlockedPermissions[featureCode])) names.push(p.name);\n }\n return names;\n}\n"],"mappings":"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","names":["buildDependsMap","permissions","present","Set","map","p","code","Map","set","dependsOn","filter","dep","has","prereqClosure","deps","out","seen","stack","length","current","pop","get","add","push","cascadeLocked","codes","directlyLocked","locked","visiting","check","viaDep","some","delete","filterGrantedByDeps","granted","ok","satisfied","every","PLATFORMS","UI_PLATFORMS","API_SURFACES","API_BUCKETS","SURFACE_BY_BUCKET","graphql","http","BUCKET_BY_SURFACE","GRAPHQL","HTTP","isApiBucket","bucket","SITE_TYPES","SERVICE_CODES","snapshotFeatureKey","code","scope","SNAPSHOT_SCHEMA_VERSION","featureAppliesAtNode","applicableSiteTypes","siteType","includes","findFeatureByCode","snapshot","code","feature","Object","values","features","undefined","buildSiteCatalog","businessCode","planCode","siteLocks","bucket","scope","availableServices","business","businesses","plans","plan","locks","catalog","sortedApps","apps","sort","a","b","name","localeCompare","app","businessAppFeatures","filter","ref","map","snapshotFeatureKey","f","isApiBucket","surfaceAllows","apiSurfaces","SURFACE_BY_BUCKET","microfrontends","web","mobile","length","membership","unlockedPermissions","memberOnBucket","sitePlatformLocked","missingServices","unmetServices","permissions","buildPermissions","lockReason","resolveLockReason","locked","unlockPlans","plansIncludingFeature","push","lucideIcon","sfSymbol","materialSymbol","remoteEntry","exposedModule","routePrefix","remoteEntryAndroid","remoteEntryIos","appCode","appName","appIcon","icon","appSortOrder","sortOrder","isPlanMember","entry","PLATFORMS","some","platform","surfaces","surface","planLocked","siteLocked","requiredServices","service","isSiteLockedOnPlatform","planMembership","planUnlocked","Set","lockEntry","perms","p","isGlobal","platforms","deps","buildDependsMap","codes","directlyPlanLocked","directlySiteLocked","has","add","directlyLocked","lockedSet","cascadeLocked","closure","prereqClosure","cascaded","planReason","c","siteReason","plansUnlockingClosure","featureCode","result","entries","unlocked","every","buildSiteRoles","roleTemplates","unionBucket","base","add","undefined","Set","composeRoleGrants","params","baseFeatures","additions","revoked","result","featureCodes","Object","keys","code","revokes","composed","bucket","PLATFORMS","merged","revoke","filter","c","includes","every","BUCKET_BY_CLIENT","web","ios","android","graphql","http","EMPTY_ROUTE","remoteEntry","exposedModule","routePrefix","resolveUserFeatures","params","snapshot","businessCode","planCode","siteLocks","platform","siteType","scope","availableServices","bucket","roleFeatures","featureByCode","code","features","snapshotFeatureKey","findFeatureByCode","catalog","buildSiteCatalog","catalogMap","Map","map","f","businessPlans","businesses","plans","currentUnlockedCodes","Set","featureCode","platforms","Object","entries","unlockedPermissions","undefined","add","planAdds","planKey","plan","adds","has","name","push","set","grantedFeatures","grant","granted","perm","get","permsSet","catalogEntry","route","isApiBucket","pickRouteForPlatform","featureDeps","buildDependsMap","permissions","permByCode","p","grantedPerms","filterGrantedByDeps","filter","c","lockedPermissions","locked","reason","lockReason","unlockPlans","missingServices","upsell","group","length","lucideIcon","sfSymbol","materialSymbol","appCode","appName","appIcon","appSortOrder","sort","a","b","localeCompare","entry","mobile","remoteEntryIos","remoteEntryAndroid","buildSiteMatrix","snapshot","businessCode","planCode","siteLocks","siteType","buildMatrix","buildPlanMatrix","allScopes","business","businesses","undefined","plans","plan","planMeta","code","name","locks","apps","app","counts","web","unlocked","total","mobile","graphql","http","features","ref","scope","feature","snapshotFeatureKey","featureAppliesAtNode","applicableSiteTypes","platforms","UI_PLATFORMS","filter","p","microfrontends","API_BUCKETS","b","apiSurfaces","includes","SURFACE_BY_BUCKET","groupByCode","Map","permissionGroups","map","g","membership","unlockedPermissions","featureInPlan","isPlanMember","siteEntry","permissions","isGlobal","cell","plat","planCodes","inPlan","selected","isSiteLockedOnPlatform","availableIn","plansUnlockingPerm","label","dependsOn","group","get","push","icon","lucideIcon","plansIncludingFeature","length","sort","a","localeCompare","featureCode","permCode","platform","excludeCode","names","Object","entries"]}
|
|
1
|
+
{"version":3,"sources":["../src/catalog-resolver/index.ts","../src/catalog-resolver/permission-deps.ts","../src/catalog-resolver/types.ts","../src/catalog-resolver/catalog.builder.ts","../src/catalog-resolver/compose-role-grants.ts","../src/catalog-resolver/resolve-user-features.ts","../src/catalog-resolver/site-matrix.builder.ts"],"sourcesContent":["// Catalog resolver — the single shared implementation of snapshot resolution (BU catalog, BU matrix, user features)\n\nexport {\n buildSiteCatalog,\n buildSiteRoles,\n featureAppliesAtNode,\n findFeatureByCode,\n isPlanMember,\n isSiteLockedOnPlatform,\n surfaceAllows,\n} from './catalog.builder';\nexport { type ComposeRoleGrantsParams, composeRoleGrants, type RevokedGrants } from './compose-role-grants';\nexport {\n buildDependsMap,\n cascadeLocked,\n type DependsMap,\n filterGrantedByDeps,\n prereqClosure,\n} from './permission-deps';\nexport {\n type ClientPlatform,\n type LockedPermission,\n type PermissionFeature,\n pickRouteForPlatform,\n type ResolveUserFeaturesParams,\n resolveUserFeatures,\n} from './resolve-user-features';\nexport {\n buildPlanMatrix,\n buildSiteMatrix,\n type SiteMatrix,\n type SiteMatrixApp,\n type SiteMatrixCell,\n type SiteMatrixFeature,\n type SiteMatrixPermission,\n} from './site-matrix.builder';\nexport {\n API_BUCKETS,\n API_SURFACES,\n type ApiBucket,\n type ApiSurface,\n BUCKET_BY_SURFACE,\n type BusinessVocabulary,\n type CatalogPermission,\n type FeatureCatalogEntry,\n type FeatureLocks,\n type FeatureUnlocks,\n isApiBucket,\n type LockReason,\n type PermissionGroupRef,\n PLATFORMS,\n type PlatformBucket,\n type PlatformCodes,\n type PlatformDenyCodes,\n type RoleItem,\n type ScopeType,\n SERVICE_CODES,\n type ServiceCode,\n SITE_TYPES,\n type SiteFeatureLocks,\n type SiteType,\n SNAPSHOT_SCHEMA_VERSION,\n type SnapshotApp,\n type SnapshotAppFeatureRef,\n type SnapshotBusiness,\n type SnapshotFeature,\n type SnapshotMicrofrontendMobile,\n type SnapshotMicrofrontends,\n type SnapshotMicrofrontendWeb,\n type SnapshotPermission,\n type SnapshotPlan,\n type SnapshotRoleTemplate,\n SURFACE_BY_BUCKET,\n snapshotFeatureKey,\n UI_PLATFORMS,\n type UiPlatformBucket,\n type VersionSnapshot,\n type VocabularyEntry,\n} from './types';\n","// Intra-feature permission prerequisites — only DIRECT edges are declared; the transitive closure is computed by recursion, cycle-guarded\n\nexport type DependsMap = Map<string, string[]>;\n\n// Builds a dependency map from a feature's permissions, keeping only edges to codes present in the set\nexport function buildDependsMap(permissions: Array<{ code: string; dependsOn?: string[] }>): DependsMap {\n const present = new Set(permissions.map((p) => p.code));\n const map: DependsMap = new Map();\n for (const p of permissions) {\n map.set(\n p.code,\n (p.dependsOn ?? []).filter((dep) => dep !== p.code && present.has(dep)),\n );\n }\n return map;\n}\n\n// Transitive prerequisite closure of a code (excludes the code itself), cycle-safe\nexport function prereqClosure(code: string, deps: DependsMap): string[] {\n const out = new Set<string>();\n const seen = new Set<string>([code]);\n const stack = [code];\n while (stack.length > 0) {\n const current = stack.pop() as string;\n for (const dep of deps.get(current) ?? []) {\n if (seen.has(dep)) continue;\n seen.add(dep);\n out.add(dep);\n stack.push(dep);\n }\n }\n return [...out];\n}\n\n// Codes locked after cascade: a code is locked if directly locked or any transitive prerequisite is (cycle-safe)\nexport function cascadeLocked(codes: string[], directlyLocked: Set<string>, deps: DependsMap): Set<string> {\n const locked = new Set<string>();\n const visiting = new Set<string>();\n const check = (code: string): boolean => {\n if (locked.has(code)) return true;\n if (directlyLocked.has(code)) {\n locked.add(code);\n return true;\n }\n if (visiting.has(code)) return false;\n visiting.add(code);\n const viaDep = (deps.get(code) ?? []).some(check);\n visiting.delete(code);\n if (viaDep) locked.add(code);\n return viaDep;\n };\n for (const code of codes) check(code);\n return locked;\n}\n\n// Keeps only codes whose FULL prerequisite closure is also present — drops a dependent missing any prerequisite (cycle-safe)\nexport function filterGrantedByDeps(granted: Set<string>, deps: DependsMap): Set<string> {\n const ok = new Set<string>();\n const visiting = new Set<string>();\n const check = (code: string): boolean => {\n if (ok.has(code)) return true;\n if (!granted.has(code)) return false;\n if (visiting.has(code)) return true;\n visiting.add(code);\n const satisfied = (deps.get(code) ?? []).every(check);\n visiting.delete(code);\n if (satisfied) ok.add(code);\n return satisfied;\n };\n for (const code of granted) check(code);\n return ok;\n}\n","// ——— Platform algebra — plan unlocks, role grants, and BU locks are all stored per platform bucket ———\n\n/**\n * The surfaces a permission can be granted on.\n *\n * `web` and `mobile` are UI buckets: a feature reaches them through a microfrontend, and a grant\n * there means a person can operate it on that surface. `graphql` and `http` are not UIs at all —\n * each is an API surface a credential signs its own requests against, so they have no\n * microfrontend and no route, and a feature needs neither to be reachable on one.\n *\n * Keeping the API buckets in the same algebra rather than beside it is what lets plan entitlement,\n * node feature locks and permission prerequisites bind an API client exactly as they bind a person.\n * One bucket per surface is what lets a plan entitle GraphQL and HTTP access independently.\n */\nexport type PlatformBucket = 'web' | 'mobile' | 'graphql' | 'http';\n\nexport const PLATFORMS: PlatformBucket[] = ['web', 'mobile', 'graphql', 'http'];\n\n/** Buckets that reach their feature through a microfrontend, and so require one to resolve. */\nexport type UiPlatformBucket = 'web' | 'mobile';\n\nexport const UI_PLATFORMS: UiPlatformBucket[] = ['web', 'mobile'];\n\n// The API surfaces an app credential can present — literally the values of core's `app_type` enum, so\n// enforcement is a plain lookup with no mapping. A feature declares which surfaces expose it.\nexport const API_SURFACES = ['GRAPHQL', 'HTTP'] as const;\nexport type ApiSurface = (typeof API_SURFACES)[number];\n\n/** Buckets that admit an API credential rather than a person — exactly one per surface. */\nexport type ApiBucket = Exclude<PlatformBucket, UiPlatformBucket>;\n\nexport const API_BUCKETS: ApiBucket[] = ['graphql', 'http'];\n\nexport const SURFACE_BY_BUCKET: Record<ApiBucket, ApiSurface> = { graphql: 'GRAPHQL', http: 'HTTP' };\nexport const BUCKET_BY_SURFACE: Record<ApiSurface, ApiBucket> = { GRAPHQL: 'graphql', HTTP: 'http' };\n\nexport function isApiBucket(bucket: PlatformBucket): bucket is ApiBucket {\n return bucket === 'graphql' || bucket === 'http';\n}\n\nexport interface PlatformCodes {\n web?: string[];\n mobile?: string[];\n graphql?: string[];\n http?: string[];\n}\n\nexport interface PlatformDenyCodes {\n web?: string[] | null;\n mobile?: string[] | null;\n graphql?: string[] | null;\n http?: string[] | null;\n}\n\nexport type FeatureUnlocks = Record<string, PlatformCodes>;\n\nexport type FeatureLocks = Record<string, PlatformDenyCodes>;\nexport type SiteFeatureLocks = FeatureLocks;\n\n// ——— Snapshot document shape — what gets stored in versions.snapshot and signed into the catalog license ———\n\nexport interface PermissionGroupRef {\n code: string;\n label: string;\n sortOrder: number;\n}\n\nexport interface SnapshotPermission {\n code: string;\n label: string;\n isGlobal: boolean;\n businesses: string[];\n dependsOn: string[];\n platforms: PlatformBucket[];\n // Code of the group this action sits under, resolved against the feature's `permissionGroups`.\n // Absent on a feature's own actions, which head the list under no heading.\n group?: string;\n}\nexport interface SnapshotMicrofrontendWeb {\n code: string;\n name: string;\n remoteEntry: string;\n exposedModule: string;\n routePrefix: string;\n}\nexport interface SnapshotMicrofrontendMobile {\n code: string;\n name: string;\n remoteEntryAndroid: string;\n remoteEntryIos: string;\n exposedModule: string;\n routePrefix: string;\n}\nexport interface SnapshotMicrofrontends {\n web?: SnapshotMicrofrontendWeb;\n mobile?: SnapshotMicrofrontendMobile;\n}\nexport type ScopeType = 'ORG' | 'LE' | 'SITE_GROUP' | 'SITE';\nexport type SiteType = 'OUTLET' | 'WAREHOUSE' | 'PRODUCTION';\nexport const SITE_TYPES: SiteType[] = ['OUTLET', 'WAREHOUSE', 'PRODUCTION'];\n// External services a feature can depend on — the org must have the service provisioned before the feature\n// unlocks. Add new services here and nowhere else in this package; every lock path is service-agnostic.\nexport const SERVICE_CODES = ['GITEA'] as const;\nexport type ServiceCode = (typeof SERVICE_CODES)[number];\nexport interface SnapshotFeature {\n code: string;\n name: string;\n lucideIcon: string;\n sfSymbol: string;\n materialSymbol: string;\n scope: ScopeType;\n applicableSiteTypes: SiteType[];\n permissions: SnapshotPermission[];\n microfrontends: SnapshotMicrofrontends;\n requiredServices: ServiceCode[];\n // The feature's sub-resources, carried once rather than repeated on each of their permissions\n permissionGroups: PermissionGroupRef[];\n // Strict — it decides which of the `graphql`/`http` buckets the feature offers at all, and `[]` offers neither\n apiSurfaces: ApiSurface[];\n}\nexport interface SnapshotAppFeatureRef {\n code: string;\n scope: ScopeType;\n}\nexport interface SnapshotApp {\n code: string;\n name: string;\n icon: string;\n sortOrder: number;\n features: SnapshotAppFeatureRef[];\n}\nexport interface SnapshotRoleTemplate {\n name: string;\n code: string;\n scope: ScopeType;\n siteType: SiteType;\n features: FeatureUnlocks;\n}\nexport interface SnapshotPlan {\n code: string;\n name: string;\n isCustom: boolean;\n maxSites: number | null;\n unlockedPermissions: FeatureUnlocks;\n}\nexport interface VocabularyEntry {\n singular: string;\n plural: string;\n}\nexport interface BusinessVocabulary {\n site?: VocabularyEntry;\n siteGroup?: VocabularyEntry;\n outlet?: VocabularyEntry;\n warehouse?: VocabularyEntry;\n production?: VocabularyEntry;\n}\nexport interface SnapshotBusiness {\n name: string;\n vocabulary?: BusinessVocabulary;\n roleTemplates: Record<string, SnapshotRoleTemplate>;\n plans: Record<string, SnapshotPlan>;\n}\nexport interface VersionSnapshot {\n schemaVersion?: number;\n // Flat feature dictionary keyed by `${scope}.${code}` (see snapshotFeatureKey) — same-code features at different scopes stay distinct\n features: Record<string, SnapshotFeature>;\n apps: SnapshotApp[];\n businesses: Record<string, SnapshotBusiness>;\n}\n\n// Composite key for the snapshot feature dictionary — feature identity is (scope, code)\nexport function snapshotFeatureKey(code: string, scope: ScopeType): string {\n return `${scope}.${code}`;\n}\n\nexport const SNAPSHOT_SCHEMA_VERSION = 6;\n\n// SERVICE = the org has not provisioned an external service the feature declares; the specific services are\n// reported alongside in `missingServices` so callers never branch on a service code baked into this union\nexport type LockReason = 'PLAN' | 'SITE' | 'SERVICE';\n\nexport interface CatalogPermission {\n code: string;\n locked: boolean;\n lockReason: LockReason | null;\n unlockPlans: string[];\n missingServices: ServiceCode[];\n}\n\nexport interface FeatureCatalogEntry {\n code: string;\n name: string;\n lucideIcon: string | null;\n sfSymbol: string;\n materialSymbol: string;\n web: {\n remoteEntry: string;\n exposedModule: string;\n routePrefix: string;\n } | null;\n mobile: {\n remoteEntryAndroid: string;\n remoteEntryIos: string;\n exposedModule: string;\n routePrefix: string;\n } | null;\n appCode: string;\n appName: string;\n appIcon: string | null;\n appSortOrder: number;\n locked: boolean;\n lockReason: LockReason | null;\n unlockPlans: string[];\n missingServices: ServiceCode[];\n permissions: CatalogPermission[];\n}\n\nexport type RoleItem = SnapshotRoleTemplate;\n","import { buildDependsMap, cascadeLocked, prereqClosure } from './permission-deps';\nimport type {\n ApiSurface,\n CatalogPermission,\n FeatureCatalogEntry,\n LockReason,\n PlatformBucket,\n PlatformCodes,\n RoleItem,\n ScopeType,\n ServiceCode,\n SiteFeatureLocks,\n SiteType,\n SnapshotFeature,\n SnapshotPlan,\n VersionSnapshot,\n} from './types';\nimport { isApiBucket, PLATFORMS, SURFACE_BY_BUCKET, snapshotFeatureKey } from './types';\n\n// Whether a feature with the given site-type applicability is exposed at this site type\nexport function featureAppliesAtNode(applicableSiteTypes: SiteType[], siteType: SiteType): boolean {\n return applicableSiteTypes.includes(siteType);\n}\n\n// Scope-agnostic lookup of a feature by bare code — grants/locks key features by code alone, so the first scope-variant's shared metadata (permission graph) answers\nexport function findFeatureByCode(snapshot: VersionSnapshot, code: string): SnapshotFeature | undefined {\n for (const feature of Object.values(snapshot.features)) {\n if (feature.code === code) return feature;\n }\n return undefined;\n}\n\n// Builds the per-site catalog for ONE platform bucket — plan is the ceiling, siteLocks is a deny-list within it; each permission carries locked + lockReason + unlockPlans\n// availableServices defaults to none, so a caller that doesn't know the org's provisioned services locks every service-dependent feature rather than leaking it\nexport function buildSiteCatalog(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n siteLocks: SiteFeatureLocks | undefined,\n bucket: PlatformBucket,\n siteType?: SiteType,\n scope?: ScopeType,\n availableServices: ServiceCode[] = [],\n): FeatureCatalogEntry[] {\n if (!businessCode) return [];\n const business = snapshot.businesses[businessCode];\n if (!business) return [];\n const plans = business.plans;\n const plan = planCode ? plans[planCode] : undefined;\n const locks = siteLocks;\n\n const catalog: FeatureCatalogEntry[] = [];\n // Iterate apps alphabetically by name so the resolved feature list (→ core-web sidebar) is app-alphabetical without any frontend re-sort\n const sortedApps = [...snapshot.apps].sort((a, b) => a.name.localeCompare(b.name));\n for (const app of sortedApps) {\n // The app's renderable features (each ref pins scope+code to one app), dropped when they don't belong to this workspace scope or node type (outlet vs container)\n const businessAppFeatures = app.features\n .filter((ref) => scope === undefined || ref.scope === scope)\n .map((ref) => snapshot.features[snapshotFeatureKey(ref.code, ref.scope)])\n .filter(\n (f): f is SnapshotFeature =>\n !!f &&\n // A UI bucket needs something to render, so a feature shipping no microfrontend is dropped.\n // An API bucket renders nothing — there a feature is admitted by the surfaces it declares\n // instead, so a GRAPHQL credential never resolves an HTTP-only feature. A surface-excluded\n // feature vanishes from the catalog entirely, which is what makes resolution fail closed.\n (isApiBucket(bucket)\n ? surfaceAllows(f.apiSurfaces, SURFACE_BY_BUCKET[bucket])\n : !!(f.microfrontends?.web || f.microfrontends?.mobile)) &&\n (siteType === undefined || featureAppliesAtNode(f.applicableSiteTypes, siteType)),\n );\n\n if (businessAppFeatures.length === 0) continue;\n\n // Emit EVERY business feature so a role's grant on a plan-omitted feature still resolves as a locked tile instead of vanishing\n for (const feature of businessAppFeatures) {\n const membership = plan?.unlockedPermissions[feature.code];\n // Routes are exposed wherever the feature SHIPS — membership never hides them\n const web = feature.microfrontends?.web;\n const mobile = feature.microfrontends?.mobile;\n\n // Feature-level lock is EXPLICIT: plan must include the feature on this bucket, the site must not null-lock\n // the platform, and every external service the feature declares must be provisioned for the org\n const memberOnBucket = membership?.[bucket] !== undefined;\n const sitePlatformLocked = locks?.[feature.code]?.[bucket] === null;\n const missingServices = unmetServices(feature, availableServices);\n // Unmet services lock every permission too — otherwise the feature reads locked while its actions still\n // report as available, which is not how plan and site locks behave\n const permissions = buildPermissions(feature, businessCode, membership, locks, plans, bucket, missingServices);\n const lockReason = resolveLockReason(!memberOnBucket, sitePlatformLocked, missingServices);\n const locked = lockReason !== null;\n const unlockPlans = lockReason === 'PLAN' ? plansIncludingFeature(plans, feature.code, bucket) : [];\n\n catalog.push({\n code: feature.code,\n name: feature.name,\n lucideIcon: feature.lucideIcon ?? null,\n sfSymbol: feature.sfSymbol ?? 'square',\n materialSymbol: feature.materialSymbol ?? 'square',\n web: web\n ? {\n remoteEntry: web.remoteEntry ?? '',\n exposedModule: web.exposedModule ?? '',\n routePrefix: web.routePrefix ?? '',\n }\n : null,\n mobile: mobile\n ? {\n remoteEntryAndroid: mobile.remoteEntryAndroid ?? '',\n remoteEntryIos: mobile.remoteEntryIos ?? '',\n exposedModule: mobile.exposedModule ?? '',\n routePrefix: mobile.routePrefix ?? '',\n }\n : null,\n appCode: app.code,\n appName: app.name,\n appIcon: app.icon ?? null,\n appSortOrder: app.sortOrder ?? 0,\n locked,\n lockReason,\n unlockPlans,\n missingServices,\n permissions,\n });\n }\n }\n return catalog;\n}\n\n// A feature is a plan member when its unlock entry exists on at least one platform (even with zero actions)\nexport function isPlanMember(entry: PlatformCodes | undefined): boolean {\n if (!entry) return false;\n return PLATFORMS.some((platform) => entry[platform] !== undefined);\n}\n\n/**\n * Whether a feature's declared API surfaces admit a caller's surface.\n *\n * Lenient only about the caller: resolving without a surface (cloud's matrix builders, UI buckets)\n * filters nothing. The declared list is always strict — including `[]`, which admits no surface.\n */\nexport function surfaceAllows(surfaces: ApiSurface[], surface: ApiSurface | undefined): boolean {\n return surface === undefined || surfaces.includes(surface);\n}\n\n// The one place lock precedence is decided, for features and permissions alike; null means nothing locks.\n// Plan is the ceiling (an unentitled feature must upsell, not send the user to provision something they still\n// couldn't use), then the site deny-list, then any unprovisioned service.\nfunction resolveLockReason(\n planLocked: boolean,\n siteLocked: boolean,\n missingServices: ServiceCode[],\n): LockReason | null {\n if (planLocked) return 'PLAN';\n if (siteLocked) return 'SITE';\n if (missingServices.length > 0) return 'SERVICE';\n return null;\n}\n\n// The services a feature declares that this org has not provisioned\nfunction unmetServices(feature: SnapshotFeature, availableServices: ServiceCode[]): ServiceCode[] {\n return feature.requiredServices.filter((service) => !availableServices.includes(service));\n}\n\n// Per-platform site-lock primitive: null locks the whole feature, string[] locks those codes, absent = not locked\nexport function isSiteLockedOnPlatform(\n entry: SiteFeatureLocks[string] | undefined,\n platform: PlatformBucket,\n code: string,\n): boolean {\n const locks = entry?.[platform];\n return locks === null || (locks?.includes(code) ?? false);\n}\n\n// A feature's business-scoped permissions, each tagged with locked + reason against the plan and site deny-list\n// (bucket-scoped). Unmet services lock the whole set — an unprovisioned service blocks every action on the feature.\nfunction buildPermissions(\n feature: SnapshotFeature,\n businessCode: string,\n planMembership: PlatformCodes | undefined,\n siteLocks: SiteFeatureLocks | undefined,\n plans: Record<string, SnapshotPlan>,\n bucket: PlatformBucket,\n missingServices: ServiceCode[] = [],\n): CatalogPermission[] {\n const planUnlocked = new Set(planMembership?.[bucket] ?? []);\n const lockEntry = siteLocks?.[feature.code];\n\n // Two filters, and the second is the point: a feature reaching this surface does not mean every\n // action under it does. A code omits the bucket when no route there enforces it, so offering it\n // would promise a capability nothing can check.\n const perms = feature.permissions\n .filter((p) => p.isGlobal || p.businesses.includes(businessCode))\n .filter((p) => p.platforms.includes(bucket));\n const deps = buildDependsMap(perms);\n const codes = perms.map((p) => p.code);\n\n // Direct plan/site locks, then cascade so a locked prerequisite (e.g. view) locks its dependents (add/edit/delete)\n const directlyPlanLocked = new Set<string>();\n const directlySiteLocked = new Set<string>();\n for (const p of perms) {\n if (!planUnlocked.has(p.code)) directlyPlanLocked.add(p.code);\n if (isSiteLockedOnPlatform(lockEntry, bucket, p.code)) directlySiteLocked.add(p.code);\n }\n const directlyLocked = new Set<string>([...directlyPlanLocked, ...directlySiteLocked]);\n const lockedSet = cascadeLocked(codes, directlyLocked, deps);\n\n return perms.map((p) => {\n // A permission is enabled only if it AND its whole prerequisite closure are unlocked — reason/upsell reflect that\n const closure = [p.code, ...prereqClosure(p.code, deps)];\n const cascaded = lockedSet.has(p.code);\n const planReason = cascaded && closure.some((c) => directlyPlanLocked.has(c));\n const siteReason = cascaded && closure.some((c) => directlySiteLocked.has(c));\n const lockReason = resolveLockReason(planReason, siteReason, missingServices);\n const locked = lockReason !== null;\n const unlockPlans = lockReason === 'PLAN' ? plansUnlockingClosure(plans, feature.code, closure, bucket) : [];\n return { code: p.code, locked, lockReason, unlockPlans, missingServices };\n });\n}\n\n// Plan codes (in the business) whose unlocked set includes the permission AND its whole prerequisite closure — upsell targets\nfunction plansUnlockingClosure(\n plans: Record<string, SnapshotPlan>,\n featureCode: string,\n closure: string[],\n bucket: PlatformBucket,\n): string[] {\n const result: string[] = [];\n for (const [code, plan] of Object.entries(plans)) {\n const unlocked = plan.unlockedPermissions[featureCode]?.[bucket];\n if (unlocked && closure.every((c) => unlocked.includes(c))) result.push(code);\n }\n return result;\n}\n\n// Plan codes (in the business) that include this feature on the bucket — the feature-level upsell targets\nfunction plansIncludingFeature(\n plans: Record<string, SnapshotPlan>,\n featureCode: string,\n bucket: PlatformBucket,\n): string[] {\n const result: string[] = [];\n for (const [code, plan] of Object.entries(plans)) {\n if (plan.unlockedPermissions[featureCode]?.[bucket] !== undefined) result.push(code);\n }\n return result;\n}\n\n// The business's role templates as provisionable role items for core (identical shapes)\nexport function buildSiteRoles(snapshot: VersionSnapshot, businessCode: string | undefined): RoleItem[] {\n if (!businessCode) return [];\n const business = snapshot.businesses[businessCode];\n if (!business) return [];\n return Object.values(business.roleTemplates);\n}\n","import { type FeatureUnlocks, PLATFORMS, type PlatformCodes, type PlatformDenyCodes } from './types';\n\nexport type RevokedGrants = Record<string, PlatformDenyCodes>;\n\nexport interface ComposeRoleGrantsParams {\n baseFeatures: FeatureUnlocks | undefined;\n additions: FeatureUnlocks;\n revoked: RevokedGrants | undefined;\n}\n\n// Deduped union of two optional code lists — undefined on both sides means no platform membership\nfunction unionBucket(base: string[] | undefined, add: string[] | undefined): string[] | undefined {\n if (base === undefined && add === undefined) return undefined;\n return [...new Set([...(base ?? []), ...(add ?? [])])];\n}\n\n// Composes a custom role's effective grants: merge(base ∪ additions) − revoked (design doc §10); inputs are never mutated\nexport function composeRoleGrants(params: ComposeRoleGrantsParams): FeatureUnlocks {\n const { baseFeatures, additions, revoked } = params;\n\n const result: FeatureUnlocks = {};\n const featureCodes = new Set([...Object.keys(baseFeatures ?? {}), ...Object.keys(additions)]);\n\n for (const code of featureCodes) {\n const base = baseFeatures?.[code] ?? {};\n const add = additions[code] ?? {};\n const revokes = revoked?.[code];\n\n const composed: PlatformCodes = {};\n for (const bucket of PLATFORMS) {\n const merged = unionBucket(base[bucket], add[bucket]);\n if (merged === undefined) continue;\n const revoke = revokes?.[bucket];\n // null revokes the whole platform (membership + all codes); string[] subtracts codes but keeps membership\n if (revoke === null) continue;\n composed[bucket] = revoke === undefined ? merged : merged.filter((c) => !revoke.includes(c));\n }\n\n // A feature with no surviving platform membership disappears from the effective set.\n // Iterates PLATFORMS rather than naming buckets — the web/mobile-only version silently\n // dropped a grant surviving only on an API bucket.\n if (PLATFORMS.every((bucket) => composed[bucket] === undefined)) continue;\n result[code] = composed;\n }\n\n return result;\n}\n","import { buildSiteCatalog, findFeatureByCode } from './catalog.builder';\nimport { buildDependsMap, filterGrantedByDeps } from './permission-deps';\nimport type {\n FeatureUnlocks,\n LockReason,\n PlatformBucket,\n ScopeType,\n ServiceCode,\n SiteFeatureLocks,\n SiteType,\n VersionSnapshot,\n} from './types';\nimport { isApiBucket, snapshotFeatureKey } from './types';\n\n/**\n * The caller's surface, as the caller reports it.\n *\n * Finer than `PlatformBucket` on the mobile side — `ios` and `android` load different remote\n * entries but share one grant bucket. The API platforms are one-to-one with their buckets: an\n * API client has no variants because it has no UI.\n */\nexport type ClientPlatform = 'web' | 'ios' | 'android' | 'graphql' | 'http';\n\n// Exhaustive by type, so adding a ClientPlatform without deciding its bucket fails the build instead\n// of silently falling through to mobile — which is how an API caller would end up resolving a UI bucket.\nconst BUCKET_BY_CLIENT: Record<ClientPlatform, PlatformBucket> = {\n web: 'web',\n ios: 'mobile',\n android: 'mobile',\n graphql: 'graphql',\n http: 'http',\n};\n\n/**\n * Stands in for the microfrontend an API client does not load.\n *\n * `PermissionFeature.route` is non-optional and read by the web sidebar and the mobile host to\n * mount a remote. Nothing on the API paths reads it — the permission interceptor uses `code`,\n * `permissions` and `locked` — so an empty route keeps one shape for every bucket instead of\n * widening the field to null across every consumer.\n */\nconst EMPTY_ROUTE = { remoteEntry: '', exposedModule: '', routePrefix: '' };\n\nexport interface LockedPermission {\n code: string;\n reason: LockReason | null;\n unlockPlans: string[];\n missingServices: ServiceCode[];\n}\n\nexport interface PlanUpsell {\n plan: string;\n features: string[];\n}\n\nexport interface PermissionFeature {\n code: string;\n name: string;\n lucideIcon: string | null;\n sfSymbol: string;\n materialSymbol: string;\n permissions: string[];\n locked: boolean;\n lockReason: LockReason | null;\n unlockPlans: string[];\n // Which declared services the org has not provisioned — empty unless lockReason is 'SERVICE'\n missingServices: ServiceCode[];\n lockedPermissions: LockedPermission[];\n upsell: PlanUpsell[];\n route: {\n remoteEntry: string;\n exposedModule: string;\n routePrefix: string;\n };\n appCode: string;\n appName: string;\n appIcon: string | null;\n appSortOrder: number;\n}\n\nexport interface ResolveUserFeaturesParams {\n snapshot: VersionSnapshot;\n businessCode: string;\n planCode: string | undefined;\n siteLocks: SiteFeatureLocks | undefined;\n roleFeatures: FeatureUnlocks;\n platform: ClientPlatform;\n siteType?: SiteType;\n scope?: ScopeType;\n // External services the org has provisioned; omitting it locks every service-dependent feature\n availableServices?: ServiceCode[];\n}\n\n// Resolves the features + MF config a user sees at a BU: plan ∧ BU catalog intersected with the role's grants, filtered to the requested platform\nexport function resolveUserFeatures(params: ResolveUserFeaturesParams): PermissionFeature[] {\n const { snapshot, businessCode, planCode, siteLocks, platform, siteType, scope, availableServices } = params;\n\n // Plan unlocks, BU locks, and role grants are stored per platform; resolve only the requesting\n // surface's bucket (web → web; ios/android → mobile; graphql/http → themselves)\n const bucket: PlatformBucket = BUCKET_BY_CLIENT[platform];\n\n const roleFeatures = params.roleFeatures;\n\n // Grants/plans/locks key features by bare code; resolve to the workspace scope's variant (or any variant when unscoped)\n const featureByCode = (code: string) =>\n scope ? snapshot.features[snapshotFeatureKey(code, scope)] : findFeatureByCode(snapshot, code);\n\n // Plan ∧ BU overlay for this bucket, filtered to features that apply to this workspace scope and node type — emits EVERY applicable business feature (plan non-members come out fully locked)\n const catalog = buildSiteCatalog(\n snapshot,\n businessCode,\n planCode,\n siteLocks,\n bucket,\n siteType,\n scope,\n availableServices,\n );\n const catalogMap = new Map(catalog.map((f) => [f.code, f]));\n\n // Per-plan feature-name delta vs the current plan — feeds the plan-locked upsell screen\n const businessPlans = snapshot.businesses[businessCode]?.plans ?? {};\n const currentUnlockedCodes = new Set<string>();\n if (planCode && businessPlans[planCode]) {\n for (const [featureCode, platforms] of Object.entries(businessPlans[planCode].unlockedPermissions)) {\n if (platforms[bucket] !== undefined) currentUnlockedCodes.add(featureCode);\n }\n }\n const planAdds = new Map<string, Array<{ code: string; name: string }>>();\n for (const [planKey, plan] of Object.entries(businessPlans)) {\n if (planKey === planCode) continue;\n const adds: Array<{ code: string; name: string }> = [];\n for (const [featureCode, platforms] of Object.entries(plan.unlockedPermissions)) {\n if (platforms[bucket] === undefined || currentUnlockedCodes.has(featureCode)) continue;\n const name = featureByCode(featureCode)?.name;\n if (name) adds.push({ code: featureCode, name });\n }\n planAdds.set(planKey, adds);\n }\n\n // Granted permission set per feature, taking only this platform's grants\n const grantedFeatures = new Map<string, Set<string>>();\n for (const [code, grant] of Object.entries(roleFeatures)) {\n // Membership is the gate: undefined = not a member on this platform; [] = member with no actions (view-only)\n const granted = grant[bucket];\n if (granted === undefined) continue;\n if (!grantedFeatures.has(code)) grantedFeatures.set(code, new Set());\n for (const perm of granted) grantedFeatures.get(code)?.add(perm);\n }\n\n // Cross-reference the granted features with the catalog to build the response\n const features: PermissionFeature[] = [];\n for (const [code, permsSet] of grantedFeatures) {\n const catalogEntry = catalogMap.get(code);\n if (!catalogEntry) continue;\n\n // A UI bucket reaches its feature by loading a microfrontend, so a feature not published to\n // this platform is omitted rather than handed over as an unloadable tile. An API client loads\n // nothing — requiring a route there would make every headless feature permanently ungrantable.\n const route = isApiBucket(bucket) ? EMPTY_ROUTE : pickRouteForPlatform(catalogEntry, platform);\n if (!route) continue;\n\n // Drop granted permissions whose intra-feature prerequisites aren't also granted (e.g. add needs view)\n const featureDeps = buildDependsMap(featureByCode(code)?.permissions ?? []);\n // Plan/BU lock a subset of permissions; surface which GRANTED ones are locked + why + how to unlock (upsell)\n const permByCode = new Map(catalogEntry.permissions.map((p) => [p.code, p]));\n // Intersected with the catalog, which now omits codes this surface does not implement. Without\n // this a grant made before the flags existed — or written straight through the API — would keep\n // resolving on a bucket where no route enforces it. The picker filtering alone is cosmetic; this\n // is what makes an unimplemented grant genuinely inert.\n const grantedPerms = [...filterGrantedByDeps(permsSet, featureDeps)].filter((c) => permByCode.has(c));\n const lockedPermissions: LockedPermission[] = grantedPerms\n .map((c) => permByCode.get(c))\n .filter((p): p is NonNullable<typeof p> => !!p?.locked)\n .map((p) => ({\n code: p.code,\n reason: p.lockReason ?? null,\n unlockPlans: p.unlockPlans,\n missingServices: p.missingServices,\n }));\n\n // For a plan-locked feature, list the extra features each unlocking plan would add (excluding this feature)\n const upsell: PlanUpsell[] =\n catalogEntry.locked && catalogEntry.lockReason === 'PLAN'\n ? catalogEntry.unlockPlans\n .map((plan) => ({\n plan,\n features: (planAdds.get(plan) ?? []).filter((f) => f.code !== code).map((f) => f.name),\n }))\n .filter((group) => group.features.length > 0)\n : [];\n\n features.push({\n code,\n name: catalogEntry.name,\n lucideIcon: catalogEntry.lucideIcon,\n sfSymbol: catalogEntry.sfSymbol,\n materialSymbol: catalogEntry.materialSymbol,\n permissions: grantedPerms,\n locked: catalogEntry.locked ?? false,\n lockReason: catalogEntry.lockReason ?? null,\n unlockPlans: catalogEntry.unlockPlans,\n missingServices: catalogEntry.missingServices,\n lockedPermissions,\n upsell,\n route,\n appCode: catalogEntry.appCode,\n appName: catalogEntry.appName,\n appIcon: catalogEntry.appIcon,\n appSortOrder: catalogEntry.appSortOrder,\n });\n }\n\n // Order app-alphabetically so the core-web sidebar (groups by app) renders apps sorted without any frontend re-sort;\n // stable sort keeps each app's features in their existing relative order\n features.sort((a, b) => a.appName.localeCompare(b.appName));\n\n return features;\n}\n\n// Selects the route block from a catalog entry for the requested platform, or null when it doesn't publish there\nexport function pickRouteForPlatform(\n entry: {\n web: {\n remoteEntry: string;\n exposedModule: string;\n routePrefix: string;\n } | null;\n mobile: {\n remoteEntryAndroid: string;\n remoteEntryIos: string;\n exposedModule: string;\n routePrefix: string;\n } | null;\n },\n platform: ClientPlatform,\n): { remoteEntry: string; exposedModule: string; routePrefix: string } | null {\n // API platforms load nothing — resolveUserFeatures never routes them here, and answering with the\n // web block for an unhandled value would hand an API caller a remote it cannot mount\n if (platform === 'graphql' || platform === 'http') return null;\n if (platform === 'ios' || platform === 'android') {\n if (!entry.mobile) return null;\n return {\n remoteEntry: platform === 'ios' ? entry.mobile.remoteEntryIos : entry.mobile.remoteEntryAndroid,\n exposedModule: entry.mobile.exposedModule,\n routePrefix: entry.mobile.routePrefix,\n };\n }\n // Web\n if (!entry.web) return null;\n return {\n remoteEntry: entry.web.remoteEntry,\n exposedModule: entry.web.exposedModule,\n routePrefix: entry.web.routePrefix,\n };\n}\n","import { featureAppliesAtNode, isPlanMember, isSiteLockedOnPlatform } from './catalog.builder';\nimport {\n API_BUCKETS,\n type ApiSurface,\n type PlatformBucket,\n type ScopeType,\n type SiteFeatureLocks,\n type SiteType,\n type SnapshotPlan,\n SURFACE_BY_BUCKET,\n snapshotFeatureKey,\n UI_PLATFORMS,\n type VersionSnapshot,\n} from './types';\n\nexport interface SiteMatrixCell {\n inPlan: boolean;\n selected: boolean;\n availableIn: string[];\n}\n\nexport interface SiteMatrixPermission {\n code: string;\n label: string;\n dependsOn: string[];\n web: SiteMatrixCell | null;\n mobile: SiteMatrixCell | null;\n graphql: SiteMatrixCell | null;\n http: SiteMatrixCell | null;\n}\n\nexport interface SiteMatrixFeature {\n code: string;\n name: string;\n icon: string | null;\n scope: ScopeType;\n applicableSiteTypes: SiteType[];\n platforms: PlatformBucket[];\n inPlan: boolean;\n availableIn: string[];\n // The API surfaces the feature declares — what lets the app-credential editor filter by the\n // credential's type.\n apiSurfaces: ApiSurface[];\n permissions: SiteMatrixPermission[];\n}\n\nexport interface MatrixCounts {\n unlocked: number;\n total: number;\n}\n\nexport interface SiteMatrixApp {\n code: string;\n name: string;\n icon: string | null;\n // Counted per surface, not as one total: a consumer showing a subset of the columns (the\n // app-credential editor shows exactly one) sums the surfaces it renders. One number covering all\n // four read as \"20/20 unlocked\" above the 5 checkboxes actually on screen.\n counts: Record<PlatformBucket, MatrixCounts>;\n features: SiteMatrixFeature[];\n}\n\nexport interface SiteMatrix {\n plan: { code: string; name: string };\n apps: SiteMatrixApp[];\n locks: SiteFeatureLocks;\n}\n\n// Builds the SITE-only apps/features/permissions matrix — not filtered to plan members; plan-locked items carry inPlan=false + availableIn\nexport function buildSiteMatrix(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n siteLocks: SiteFeatureLocks | undefined,\n siteType?: SiteType,\n): SiteMatrix {\n return buildMatrix(snapshot, businessCode, planCode, siteLocks, false, siteType);\n}\n\n// Builds the all-scopes apps/features/permissions matrix — every scope's features included, each carrying its real scope; powers the Plan Overview + Create Custom Role picker\nexport function buildPlanMatrix(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n siteLocks?: SiteFeatureLocks,\n): SiteMatrix {\n return buildMatrix(snapshot, businessCode, planCode, siteLocks, true);\n}\n\n// Shared matrix builder — allScopes=false keeps only SITE refs; allScopes=true includes every scope and emits each feature's real scope\nfunction buildMatrix(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n siteLocks: SiteFeatureLocks | undefined,\n allScopes: boolean,\n siteType?: SiteType,\n): SiteMatrix {\n const business = businessCode ? snapshot.businesses[businessCode] : undefined;\n const plans = business?.plans ?? {};\n const plan = planCode ? plans[planCode] : undefined;\n const planMeta = { code: planCode ?? '', name: plan?.name ?? planCode ?? '' };\n const locks = siteLocks ?? {};\n if (!business || !plan) return { plan: planMeta, apps: [], locks };\n\n const apps: SiteMatrixApp[] = [];\n for (const app of snapshot.apps) {\n const counts: Record<PlatformBucket, MatrixCounts> = {\n web: { unlocked: 0, total: 0 },\n mobile: { unlocked: 0, total: 0 },\n graphql: { unlocked: 0, total: 0 },\n http: { unlocked: 0, total: 0 },\n };\n const features: SiteMatrixFeature[] = [];\n\n for (const ref of app.features) {\n if (!allScopes && ref.scope !== 'SITE') continue;\n const code = ref.code;\n const feature = snapshot.features[snapshotFeatureKey(code, ref.scope)];\n if (!feature) continue;\n if (siteType !== undefined && !featureAppliesAtNode(feature.applicableSiteTypes, siteType)) continue;\n // A UI bucket is offered only where the feature publishes a microfrontend; each API bucket is\n // offered where the feature declares its surface. An undeclared surface shows an em dash like\n // a missing microfrontend does.\n const platforms: PlatformBucket[] = [\n ...UI_PLATFORMS.filter((p) => !!feature.microfrontends?.[p]),\n ...API_BUCKETS.filter((b) => feature.apiSurfaces.includes(SURFACE_BY_BUCKET[b])),\n ];\n\n const groupByCode = new Map(feature.permissionGroups.map((g) => [g.code, g]));\n const membership = plan.unlockedPermissions[code];\n const featureInPlan = isPlanMember(membership);\n const siteEntry = siteLocks?.[code];\n\n const permissions: SiteMatrixPermission[] = feature.permissions\n .filter((p) => p.isGlobal || p.businesses.includes(businessCode ?? ''))\n .map((p) => {\n const cell = (plat: PlatformBucket): SiteMatrixCell | null => {\n // The feature must reach this bucket AND this code must be implemented on it — the same\n // two gates buildSiteCatalog applies, so the matrix and the catalog cannot disagree\n if (!platforms.includes(plat) || !p.platforms.includes(plat)) return null;\n const planCodes = membership?.[plat];\n const inPlan = featureInPlan && planCodes !== undefined && planCodes.includes(p.code);\n // Deny-list: an in-plan cell is selected unless the site locks it on this platform\n const selected = inPlan && !isSiteLockedOnPlatform(siteEntry, plat, p.code);\n const availableIn = inPlan ? [] : plansUnlockingPerm(plans, code, p.code, plat, planCode);\n counts[plat].total += 1;\n if (inPlan) counts[plat].unlocked += 1;\n return { inPlan, selected, availableIn };\n };\n return {\n code: p.code,\n label: p.label,\n dependsOn: p.dependsOn,\n group: p.group ? groupByCode.get(p.group) : undefined,\n web: cell('web'),\n mobile: cell('mobile'),\n graphql: cell('graphql'),\n http: cell('http'),\n };\n });\n\n features.push({\n code: feature.code,\n name: feature.name,\n icon: feature.lucideIcon ?? null,\n scope: feature.scope,\n applicableSiteTypes: feature.applicableSiteTypes,\n platforms,\n inPlan: featureInPlan,\n availableIn: featureInPlan ? [] : plansIncludingFeature(plans, code, planCode),\n apiSurfaces: feature.apiSurfaces,\n permissions,\n });\n }\n\n if (features.length === 0) continue;\n apps.push({ code: app.code, name: app.name, icon: app.icon ?? null, counts, features });\n }\n\n // Emit apps alphabetically by name so every consumer (Plan Overview, Role picker, all Locks screens) renders them sorted\n apps.sort((a, b) => a.name.localeCompare(b.name));\n\n return { plan: planMeta, apps, locks };\n}\n\n// Names of other plans (excluding the org's own) that unlock this feature+permission on the given platform\nfunction plansUnlockingPerm(\n plans: Record<string, SnapshotPlan>,\n featureCode: string,\n permCode: string,\n platform: PlatformBucket,\n excludeCode: string | undefined,\n): string[] {\n const names: string[] = [];\n for (const [code, p] of Object.entries(plans)) {\n if (code === excludeCode) continue;\n if ((p.unlockedPermissions[featureCode]?.[platform] ?? []).includes(permCode)) names.push(p.name);\n }\n return names;\n}\n\n// Names of other plans (excluding the org's own) that include this feature at all (membership) — feature-level upsell\nfunction plansIncludingFeature(\n plans: Record<string, SnapshotPlan>,\n featureCode: string,\n excludeCode: string | undefined,\n): string[] {\n const names: string[] = [];\n for (const [code, p] of Object.entries(plans)) {\n if (code === excludeCode) continue;\n if (isPlanMember(p.unlockedPermissions[featureCode])) names.push(p.name);\n }\n return names;\n}\n"],"mappings":"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","names":["buildDependsMap","permissions","present","Set","map","p","code","Map","set","dependsOn","filter","dep","has","prereqClosure","deps","out","seen","stack","length","current","pop","get","add","push","cascadeLocked","codes","directlyLocked","locked","visiting","check","viaDep","some","delete","filterGrantedByDeps","granted","ok","satisfied","every","PLATFORMS","UI_PLATFORMS","API_SURFACES","API_BUCKETS","SURFACE_BY_BUCKET","graphql","http","BUCKET_BY_SURFACE","GRAPHQL","HTTP","isApiBucket","bucket","SITE_TYPES","SERVICE_CODES","snapshotFeatureKey","code","scope","SNAPSHOT_SCHEMA_VERSION","featureAppliesAtNode","applicableSiteTypes","siteType","includes","findFeatureByCode","snapshot","code","feature","Object","values","features","undefined","buildSiteCatalog","businessCode","planCode","siteLocks","bucket","scope","availableServices","business","businesses","plans","plan","locks","catalog","sortedApps","apps","sort","a","b","name","localeCompare","app","businessAppFeatures","filter","ref","map","snapshotFeatureKey","f","isApiBucket","surfaceAllows","apiSurfaces","SURFACE_BY_BUCKET","microfrontends","web","mobile","length","membership","unlockedPermissions","memberOnBucket","sitePlatformLocked","missingServices","unmetServices","permissions","buildPermissions","lockReason","resolveLockReason","locked","unlockPlans","plansIncludingFeature","push","lucideIcon","sfSymbol","materialSymbol","remoteEntry","exposedModule","routePrefix","remoteEntryAndroid","remoteEntryIos","appCode","appName","appIcon","icon","appSortOrder","sortOrder","isPlanMember","entry","PLATFORMS","some","platform","surfaces","surface","planLocked","siteLocked","requiredServices","service","isSiteLockedOnPlatform","planMembership","planUnlocked","Set","lockEntry","perms","p","isGlobal","platforms","deps","buildDependsMap","codes","directlyPlanLocked","directlySiteLocked","has","add","directlyLocked","lockedSet","cascadeLocked","closure","prereqClosure","cascaded","planReason","c","siteReason","plansUnlockingClosure","featureCode","result","entries","unlocked","every","buildSiteRoles","roleTemplates","unionBucket","base","add","undefined","Set","composeRoleGrants","params","baseFeatures","additions","revoked","result","featureCodes","Object","keys","code","revokes","composed","bucket","PLATFORMS","merged","revoke","filter","c","includes","every","BUCKET_BY_CLIENT","web","ios","android","graphql","http","EMPTY_ROUTE","remoteEntry","exposedModule","routePrefix","resolveUserFeatures","params","snapshot","businessCode","planCode","siteLocks","platform","siteType","scope","availableServices","bucket","roleFeatures","featureByCode","code","features","snapshotFeatureKey","findFeatureByCode","catalog","buildSiteCatalog","catalogMap","Map","map","f","businessPlans","businesses","plans","currentUnlockedCodes","Set","featureCode","platforms","Object","entries","unlockedPermissions","undefined","add","planAdds","planKey","plan","adds","has","name","push","set","grantedFeatures","grant","granted","perm","get","permsSet","catalogEntry","route","isApiBucket","pickRouteForPlatform","featureDeps","buildDependsMap","permissions","permByCode","p","grantedPerms","filterGrantedByDeps","filter","c","lockedPermissions","locked","reason","lockReason","unlockPlans","missingServices","upsell","group","length","lucideIcon","sfSymbol","materialSymbol","appCode","appName","appIcon","appSortOrder","sort","a","b","localeCompare","entry","mobile","remoteEntryIos","remoteEntryAndroid","buildSiteMatrix","snapshot","businessCode","planCode","siteLocks","siteType","buildMatrix","buildPlanMatrix","allScopes","business","businesses","undefined","plans","plan","planMeta","code","name","locks","apps","app","counts","web","unlocked","total","mobile","graphql","http","features","ref","scope","feature","snapshotFeatureKey","featureAppliesAtNode","applicableSiteTypes","platforms","UI_PLATFORMS","filter","p","microfrontends","API_BUCKETS","b","apiSurfaces","includes","SURFACE_BY_BUCKET","groupByCode","Map","permissionGroups","map","g","membership","unlockedPermissions","featureInPlan","isPlanMember","siteEntry","permissions","isGlobal","cell","plat","planCodes","inPlan","selected","isSiteLockedOnPlatform","availableIn","plansUnlockingPerm","label","dependsOn","group","get","push","icon","lucideIcon","plansIncludingFeature","length","sort","a","localeCompare","featureCode","permCode","platform","excludeCode","names","Object","entries"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { V as VersionSnapshot, S as SiteFeatureLocks, P as PlatformBucket, a as SiteType, b as ScopeType, c as ServiceCode, F as FeatureCatalogEntry, R as RoleItem, d as SnapshotFeature, e as PlatformCodes, A as ApiSurface, f as FeatureUnlocks, g as PlatformDenyCodes, L as LockReason } from './types-
|
|
2
|
-
export { h as API_BUCKETS, i as API_SURFACES, j as ApiBucket, B as BUCKET_BY_SURFACE, k as BusinessVocabulary, C as CatalogPermission, l as FeatureLocks, o as PLATFORMS, n as PermissionGroupRef, p as SERVICE_CODES, q as SITE_TYPES, r as SNAPSHOT_SCHEMA_VERSION, E as SURFACE_BY_BUCKET, s as SnapshotApp, t as SnapshotAppFeatureRef, u as SnapshotBusiness, v as SnapshotMicrofrontendMobile, x as SnapshotMicrofrontendWeb, w as SnapshotMicrofrontends, y as SnapshotPermission, z as SnapshotPlan, D as SnapshotRoleTemplate, U as UI_PLATFORMS, H as UiPlatformBucket, I as VocabularyEntry, m as isApiBucket, G as snapshotFeatureKey } from './types-
|
|
1
|
+
import { V as VersionSnapshot, S as SiteFeatureLocks, P as PlatformBucket, a as SiteType, b as ScopeType, c as ServiceCode, F as FeatureCatalogEntry, R as RoleItem, d as SnapshotFeature, e as PlatformCodes, A as ApiSurface, f as FeatureUnlocks, g as PlatformDenyCodes, L as LockReason } from './types-BQY0Aa1p.cjs';
|
|
2
|
+
export { h as API_BUCKETS, i as API_SURFACES, j as ApiBucket, B as BUCKET_BY_SURFACE, k as BusinessVocabulary, C as CatalogPermission, l as FeatureLocks, o as PLATFORMS, n as PermissionGroupRef, p as SERVICE_CODES, q as SITE_TYPES, r as SNAPSHOT_SCHEMA_VERSION, E as SURFACE_BY_BUCKET, s as SnapshotApp, t as SnapshotAppFeatureRef, u as SnapshotBusiness, v as SnapshotMicrofrontendMobile, x as SnapshotMicrofrontendWeb, w as SnapshotMicrofrontends, y as SnapshotPermission, z as SnapshotPlan, D as SnapshotRoleTemplate, U as UI_PLATFORMS, H as UiPlatformBucket, I as VocabularyEntry, m as isApiBucket, G as snapshotFeatureKey } from './types-BQY0Aa1p.cjs';
|
|
3
3
|
|
|
4
4
|
declare function featureAppliesAtNode(applicableSiteTypes: SiteType[], siteType: SiteType): boolean;
|
|
5
5
|
declare function findFeatureByCode(snapshot: VersionSnapshot, code: string): SnapshotFeature | undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { V as VersionSnapshot, S as SiteFeatureLocks, P as PlatformBucket, a as SiteType, b as ScopeType, c as ServiceCode, F as FeatureCatalogEntry, R as RoleItem, d as SnapshotFeature, e as PlatformCodes, A as ApiSurface, f as FeatureUnlocks, g as PlatformDenyCodes, L as LockReason } from './types-
|
|
2
|
-
export { h as API_BUCKETS, i as API_SURFACES, j as ApiBucket, B as BUCKET_BY_SURFACE, k as BusinessVocabulary, C as CatalogPermission, l as FeatureLocks, o as PLATFORMS, n as PermissionGroupRef, p as SERVICE_CODES, q as SITE_TYPES, r as SNAPSHOT_SCHEMA_VERSION, E as SURFACE_BY_BUCKET, s as SnapshotApp, t as SnapshotAppFeatureRef, u as SnapshotBusiness, v as SnapshotMicrofrontendMobile, x as SnapshotMicrofrontendWeb, w as SnapshotMicrofrontends, y as SnapshotPermission, z as SnapshotPlan, D as SnapshotRoleTemplate, U as UI_PLATFORMS, H as UiPlatformBucket, I as VocabularyEntry, m as isApiBucket, G as snapshotFeatureKey } from './types-
|
|
1
|
+
import { V as VersionSnapshot, S as SiteFeatureLocks, P as PlatformBucket, a as SiteType, b as ScopeType, c as ServiceCode, F as FeatureCatalogEntry, R as RoleItem, d as SnapshotFeature, e as PlatformCodes, A as ApiSurface, f as FeatureUnlocks, g as PlatformDenyCodes, L as LockReason } from './types-BQY0Aa1p.js';
|
|
2
|
+
export { h as API_BUCKETS, i as API_SURFACES, j as ApiBucket, B as BUCKET_BY_SURFACE, k as BusinessVocabulary, C as CatalogPermission, l as FeatureLocks, o as PLATFORMS, n as PermissionGroupRef, p as SERVICE_CODES, q as SITE_TYPES, r as SNAPSHOT_SCHEMA_VERSION, E as SURFACE_BY_BUCKET, s as SnapshotApp, t as SnapshotAppFeatureRef, u as SnapshotBusiness, v as SnapshotMicrofrontendMobile, x as SnapshotMicrofrontendWeb, w as SnapshotMicrofrontends, y as SnapshotPermission, z as SnapshotPlan, D as SnapshotRoleTemplate, U as UI_PLATFORMS, H as UiPlatformBucket, I as VocabularyEntry, m as isApiBucket, G as snapshotFeatureKey } from './types-BQY0Aa1p.js';
|
|
3
3
|
|
|
4
4
|
declare function featureAppliesAtNode(applicableSiteTypes: SiteType[], siteType: SiteType): boolean;
|
|
5
5
|
declare function findFeatureByCode(snapshot: VersionSnapshot, code: string): SnapshotFeature | undefined;
|
package/dist/catalog-resolver.js
CHANGED
|
@@ -114,7 +114,7 @@ function snapshotFeatureKey(code, scope) {
|
|
|
114
114
|
return `${scope}.${code}`;
|
|
115
115
|
}
|
|
116
116
|
__name(snapshotFeatureKey, "snapshotFeatureKey");
|
|
117
|
-
var SNAPSHOT_SCHEMA_VERSION =
|
|
117
|
+
var SNAPSHOT_SCHEMA_VERSION = 6;
|
|
118
118
|
|
|
119
119
|
// src/catalog-resolver/catalog.builder.ts
|
|
120
120
|
function featureAppliesAtNode(applicableSiteTypes, siteType) {
|
|
@@ -137,7 +137,7 @@ function buildSiteCatalog(snapshot, businessCode, planCode, siteLocks, bucket, s
|
|
|
137
137
|
const locks = siteLocks;
|
|
138
138
|
const catalog = [];
|
|
139
139
|
const sortedApps = [
|
|
140
|
-
...
|
|
140
|
+
...snapshot.apps
|
|
141
141
|
].sort((a, b) => a.name.localeCompare(b.name));
|
|
142
142
|
for (const app of sortedApps) {
|
|
143
143
|
const businessAppFeatures = app.features.filter((ref) => scope === void 0 || ref.scope === scope).map((ref) => snapshot.features[snapshotFeatureKey(ref.code, ref.scope)]).filter((f) => !!f && // A UI bucket needs something to render, so a feature shipping no microfrontend is dropped.
|
|
@@ -456,7 +456,7 @@ function buildMatrix(snapshot, businessCode, planCode, siteLocks, allScopes, sit
|
|
|
456
456
|
locks
|
|
457
457
|
};
|
|
458
458
|
const apps = [];
|
|
459
|
-
for (const app of
|
|
459
|
+
for (const app of snapshot.apps) {
|
|
460
460
|
const counts = {
|
|
461
461
|
web: {
|
|
462
462
|
unlocked: 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/catalog-resolver/permission-deps.ts","../src/catalog-resolver/types.ts","../src/catalog-resolver/catalog.builder.ts","../src/catalog-resolver/compose-role-grants.ts","../src/catalog-resolver/resolve-user-features.ts","../src/catalog-resolver/site-matrix.builder.ts"],"sourcesContent":["// Intra-feature permission prerequisites — only DIRECT edges are declared; the transitive closure is computed by recursion, cycle-guarded\n\nexport type DependsMap = Map<string, string[]>;\n\n// Builds a dependency map from a feature's permissions, keeping only edges to codes present in the set\nexport function buildDependsMap(permissions: Array<{ code: string; dependsOn?: string[] }>): DependsMap {\n const present = new Set(permissions.map((p) => p.code));\n const map: DependsMap = new Map();\n for (const p of permissions) {\n map.set(\n p.code,\n (p.dependsOn ?? []).filter((dep) => dep !== p.code && present.has(dep)),\n );\n }\n return map;\n}\n\n// Transitive prerequisite closure of a code (excludes the code itself), cycle-safe\nexport function prereqClosure(code: string, deps: DependsMap): string[] {\n const out = new Set<string>();\n const seen = new Set<string>([code]);\n const stack = [code];\n while (stack.length > 0) {\n const current = stack.pop() as string;\n for (const dep of deps.get(current) ?? []) {\n if (seen.has(dep)) continue;\n seen.add(dep);\n out.add(dep);\n stack.push(dep);\n }\n }\n return [...out];\n}\n\n// Codes locked after cascade: a code is locked if directly locked or any transitive prerequisite is (cycle-safe)\nexport function cascadeLocked(codes: string[], directlyLocked: Set<string>, deps: DependsMap): Set<string> {\n const locked = new Set<string>();\n const visiting = new Set<string>();\n const check = (code: string): boolean => {\n if (locked.has(code)) return true;\n if (directlyLocked.has(code)) {\n locked.add(code);\n return true;\n }\n if (visiting.has(code)) return false;\n visiting.add(code);\n const viaDep = (deps.get(code) ?? []).some(check);\n visiting.delete(code);\n if (viaDep) locked.add(code);\n return viaDep;\n };\n for (const code of codes) check(code);\n return locked;\n}\n\n// Keeps only codes whose FULL prerequisite closure is also present — drops a dependent missing any prerequisite (cycle-safe)\nexport function filterGrantedByDeps(granted: Set<string>, deps: DependsMap): Set<string> {\n const ok = new Set<string>();\n const visiting = new Set<string>();\n const check = (code: string): boolean => {\n if (ok.has(code)) return true;\n if (!granted.has(code)) return false;\n if (visiting.has(code)) return true;\n visiting.add(code);\n const satisfied = (deps.get(code) ?? []).every(check);\n visiting.delete(code);\n if (satisfied) ok.add(code);\n return satisfied;\n };\n for (const code of granted) check(code);\n return ok;\n}\n","// ——— Platform algebra — plan unlocks, role grants, and BU locks are all stored per platform bucket ———\n\n/**\n * The surfaces a permission can be granted on.\n *\n * `web` and `mobile` are UI buckets: a feature reaches them through a microfrontend, and a grant\n * there means a person can operate it on that surface. `graphql` and `http` are not UIs at all —\n * each is an API surface a credential signs its own requests against, so they have no\n * microfrontend and no route, and a feature needs neither to be reachable on one.\n *\n * Keeping the API buckets in the same algebra rather than beside it is what lets plan entitlement,\n * node feature locks and permission prerequisites bind an API client exactly as they bind a person.\n * One bucket per surface is what lets a plan entitle GraphQL and HTTP access independently.\n */\nexport type PlatformBucket = 'web' | 'mobile' | 'graphql' | 'http';\n\nexport const PLATFORMS: PlatformBucket[] = ['web', 'mobile', 'graphql', 'http'];\n\n/** Buckets that reach their feature through a microfrontend, and so require one to resolve. */\nexport type UiPlatformBucket = 'web' | 'mobile';\n\nexport const UI_PLATFORMS: UiPlatformBucket[] = ['web', 'mobile'];\n\n// The API surfaces an app credential can present — literally the values of core's `app_type` enum, so\n// enforcement is a plain lookup with no mapping. A feature declares which surfaces expose it.\nexport const API_SURFACES = ['GRAPHQL', 'HTTP'] as const;\nexport type ApiSurface = (typeof API_SURFACES)[number];\n\n/** Buckets that admit an API credential rather than a person — exactly one per surface. */\nexport type ApiBucket = Exclude<PlatformBucket, UiPlatformBucket>;\n\nexport const API_BUCKETS: ApiBucket[] = ['graphql', 'http'];\n\nexport const SURFACE_BY_BUCKET: Record<ApiBucket, ApiSurface> = { graphql: 'GRAPHQL', http: 'HTTP' };\nexport const BUCKET_BY_SURFACE: Record<ApiSurface, ApiBucket> = { GRAPHQL: 'graphql', HTTP: 'http' };\n\nexport function isApiBucket(bucket: PlatformBucket): bucket is ApiBucket {\n return bucket === 'graphql' || bucket === 'http';\n}\n\nexport interface PlatformCodes {\n web?: string[];\n mobile?: string[];\n graphql?: string[];\n http?: string[];\n}\n\nexport interface PlatformDenyCodes {\n web?: string[] | null;\n mobile?: string[] | null;\n graphql?: string[] | null;\n http?: string[] | null;\n}\n\nexport type FeatureUnlocks = Record<string, PlatformCodes>;\n\nexport type FeatureLocks = Record<string, PlatformDenyCodes>;\nexport type SiteFeatureLocks = FeatureLocks;\n\n// ——— Snapshot document shape — what gets stored in versions.snapshot and signed into the catalog license ———\n\nexport interface PermissionGroupRef {\n code: string;\n label: string;\n sortOrder: number;\n}\n\nexport interface SnapshotPermission {\n code: string;\n label: string;\n isGlobal: boolean;\n businesses: string[];\n dependsOn: string[];\n platforms: PlatformBucket[];\n // Code of the group this action sits under, resolved against the feature's `permissionGroups`.\n // Absent on a feature's own actions, which head the list under no heading.\n group?: string;\n}\nexport interface SnapshotMicrofrontendWeb {\n code: string;\n name: string;\n remoteEntry: string;\n exposedModule: string;\n routePrefix: string;\n}\nexport interface SnapshotMicrofrontendMobile {\n code: string;\n name: string;\n remoteEntryAndroid: string;\n remoteEntryIos: string;\n exposedModule: string;\n routePrefix: string;\n}\nexport interface SnapshotMicrofrontends {\n web?: SnapshotMicrofrontendWeb;\n mobile?: SnapshotMicrofrontendMobile;\n}\nexport type ScopeType = 'ORG' | 'LE' | 'SITE_GROUP' | 'SITE';\nexport type SiteType = 'OUTLET' | 'WAREHOUSE' | 'PRODUCTION';\nexport const SITE_TYPES: SiteType[] = ['OUTLET', 'WAREHOUSE', 'PRODUCTION'];\n// External services a feature can depend on — the org must have the service provisioned before the feature\n// unlocks. Add new services here and nowhere else in this package; every lock path is service-agnostic.\nexport const SERVICE_CODES = ['GITEA'] as const;\nexport type ServiceCode = (typeof SERVICE_CODES)[number];\nexport interface SnapshotFeature {\n code: string;\n name: string;\n lucideIcon: string;\n sfSymbol: string;\n materialSymbol: string;\n scope: ScopeType;\n applicableSiteTypes: SiteType[];\n permissions: SnapshotPermission[];\n microfrontends: SnapshotMicrofrontends;\n requiredServices: ServiceCode[];\n // The feature's sub-resources, carried once rather than repeated on each of their permissions\n permissionGroups: PermissionGroupRef[];\n // Strict — it decides which of the `graphql`/`http` buckets the feature offers at all, and `[]` offers neither\n apiSurfaces: ApiSurface[];\n}\nexport interface SnapshotAppFeatureRef {\n code: string;\n scope: ScopeType;\n}\nexport interface SnapshotApp {\n code: string;\n name: string;\n icon: string;\n sortOrder: number;\n features: SnapshotAppFeatureRef[];\n}\nexport interface SnapshotRoleTemplate {\n name: string;\n code: string;\n scope: ScopeType;\n siteType: SiteType;\n features: FeatureUnlocks;\n}\nexport interface SnapshotPlan {\n code: string;\n name: string;\n isCustom: boolean;\n maxSites: number | null;\n unlockedPermissions: FeatureUnlocks;\n}\nexport interface VocabularyEntry {\n singular: string;\n plural: string;\n}\nexport interface BusinessVocabulary {\n site?: VocabularyEntry;\n siteGroup?: VocabularyEntry;\n outlet?: VocabularyEntry;\n warehouse?: VocabularyEntry;\n production?: VocabularyEntry;\n}\nexport interface SnapshotBusiness {\n name: string;\n vocabulary?: BusinessVocabulary;\n apps: SnapshotApp[];\n roleTemplates: Record<string, SnapshotRoleTemplate>;\n plans: Record<string, SnapshotPlan>;\n}\nexport interface VersionSnapshot {\n schemaVersion?: number;\n // Flat feature dictionary keyed by `${scope}.${code}` (see snapshotFeatureKey) — same-code features at different scopes stay distinct\n features: Record<string, SnapshotFeature>;\n businesses: Record<string, SnapshotBusiness>;\n}\n\n// Composite key for the snapshot feature dictionary — feature identity is (scope, code)\nexport function snapshotFeatureKey(code: string, scope: ScopeType): string {\n return `${scope}.${code}`;\n}\n\nexport const SNAPSHOT_SCHEMA_VERSION = 5;\n\n// SERVICE = the org has not provisioned an external service the feature declares; the specific services are\n// reported alongside in `missingServices` so callers never branch on a service code baked into this union\nexport type LockReason = 'PLAN' | 'SITE' | 'SERVICE';\n\nexport interface CatalogPermission {\n code: string;\n locked: boolean;\n lockReason: LockReason | null;\n unlockPlans: string[];\n missingServices: ServiceCode[];\n}\n\nexport interface FeatureCatalogEntry {\n code: string;\n name: string;\n lucideIcon: string | null;\n sfSymbol: string;\n materialSymbol: string;\n web: {\n remoteEntry: string;\n exposedModule: string;\n routePrefix: string;\n } | null;\n mobile: {\n remoteEntryAndroid: string;\n remoteEntryIos: string;\n exposedModule: string;\n routePrefix: string;\n } | null;\n appCode: string;\n appName: string;\n appIcon: string | null;\n appSortOrder: number;\n locked: boolean;\n lockReason: LockReason | null;\n unlockPlans: string[];\n missingServices: ServiceCode[];\n permissions: CatalogPermission[];\n}\n\nexport type RoleItem = SnapshotRoleTemplate;\n","import { buildDependsMap, cascadeLocked, prereqClosure } from './permission-deps';\nimport type {\n ApiSurface,\n CatalogPermission,\n FeatureCatalogEntry,\n LockReason,\n PlatformBucket,\n PlatformCodes,\n RoleItem,\n ScopeType,\n ServiceCode,\n SiteFeatureLocks,\n SiteType,\n SnapshotFeature,\n SnapshotPlan,\n VersionSnapshot,\n} from './types';\nimport { isApiBucket, PLATFORMS, SURFACE_BY_BUCKET, snapshotFeatureKey } from './types';\n\n// Whether a feature with the given site-type applicability is exposed at this site type\nexport function featureAppliesAtNode(applicableSiteTypes: SiteType[], siteType: SiteType): boolean {\n return applicableSiteTypes.includes(siteType);\n}\n\n// Scope-agnostic lookup of a feature by bare code — grants/locks key features by code alone, so the first scope-variant's shared metadata (permission graph) answers\nexport function findFeatureByCode(snapshot: VersionSnapshot, code: string): SnapshotFeature | undefined {\n for (const feature of Object.values(snapshot.features)) {\n if (feature.code === code) return feature;\n }\n return undefined;\n}\n\n// Builds the per-site catalog for ONE platform bucket — plan is the ceiling, siteLocks is a deny-list within it; each permission carries locked + lockReason + unlockPlans\n// availableServices defaults to none, so a caller that doesn't know the org's provisioned services locks every service-dependent feature rather than leaking it\nexport function buildSiteCatalog(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n siteLocks: SiteFeatureLocks | undefined,\n bucket: PlatformBucket,\n siteType?: SiteType,\n scope?: ScopeType,\n availableServices: ServiceCode[] = [],\n): FeatureCatalogEntry[] {\n if (!businessCode) return [];\n const business = snapshot.businesses[businessCode];\n if (!business) return [];\n const plans = business.plans;\n const plan = planCode ? plans[planCode] : undefined;\n const locks = siteLocks;\n\n const catalog: FeatureCatalogEntry[] = [];\n // Iterate apps alphabetically by name so the resolved feature list (→ core-web sidebar) is app-alphabetical without any frontend re-sort\n const sortedApps = [...business.apps].sort((a, b) => a.name.localeCompare(b.name));\n for (const app of sortedApps) {\n // The app's renderable features (each ref pins scope+code to one app), dropped when they don't belong to this workspace scope or node type (outlet vs container)\n const businessAppFeatures = app.features\n .filter((ref) => scope === undefined || ref.scope === scope)\n .map((ref) => snapshot.features[snapshotFeatureKey(ref.code, ref.scope)])\n .filter(\n (f): f is SnapshotFeature =>\n !!f &&\n // A UI bucket needs something to render, so a feature shipping no microfrontend is dropped.\n // An API bucket renders nothing — there a feature is admitted by the surfaces it declares\n // instead, so a GRAPHQL credential never resolves an HTTP-only feature. A surface-excluded\n // feature vanishes from the catalog entirely, which is what makes resolution fail closed.\n (isApiBucket(bucket)\n ? surfaceAllows(f.apiSurfaces, SURFACE_BY_BUCKET[bucket])\n : !!(f.microfrontends?.web || f.microfrontends?.mobile)) &&\n (siteType === undefined || featureAppliesAtNode(f.applicableSiteTypes, siteType)),\n );\n\n if (businessAppFeatures.length === 0) continue;\n\n // Emit EVERY business feature so a role's grant on a plan-omitted feature still resolves as a locked tile instead of vanishing\n for (const feature of businessAppFeatures) {\n const membership = plan?.unlockedPermissions[feature.code];\n // Routes are exposed wherever the feature SHIPS — membership never hides them\n const web = feature.microfrontends?.web;\n const mobile = feature.microfrontends?.mobile;\n\n // Feature-level lock is EXPLICIT: plan must include the feature on this bucket, the site must not null-lock\n // the platform, and every external service the feature declares must be provisioned for the org\n const memberOnBucket = membership?.[bucket] !== undefined;\n const sitePlatformLocked = locks?.[feature.code]?.[bucket] === null;\n const missingServices = unmetServices(feature, availableServices);\n // Unmet services lock every permission too — otherwise the feature reads locked while its actions still\n // report as available, which is not how plan and site locks behave\n const permissions = buildPermissions(feature, businessCode, membership, locks, plans, bucket, missingServices);\n const lockReason = resolveLockReason(!memberOnBucket, sitePlatformLocked, missingServices);\n const locked = lockReason !== null;\n const unlockPlans = lockReason === 'PLAN' ? plansIncludingFeature(plans, feature.code, bucket) : [];\n\n catalog.push({\n code: feature.code,\n name: feature.name,\n lucideIcon: feature.lucideIcon ?? null,\n sfSymbol: feature.sfSymbol ?? 'square',\n materialSymbol: feature.materialSymbol ?? 'square',\n web: web\n ? {\n remoteEntry: web.remoteEntry ?? '',\n exposedModule: web.exposedModule ?? '',\n routePrefix: web.routePrefix ?? '',\n }\n : null,\n mobile: mobile\n ? {\n remoteEntryAndroid: mobile.remoteEntryAndroid ?? '',\n remoteEntryIos: mobile.remoteEntryIos ?? '',\n exposedModule: mobile.exposedModule ?? '',\n routePrefix: mobile.routePrefix ?? '',\n }\n : null,\n appCode: app.code,\n appName: app.name,\n appIcon: app.icon ?? null,\n appSortOrder: app.sortOrder ?? 0,\n locked,\n lockReason,\n unlockPlans,\n missingServices,\n permissions,\n });\n }\n }\n return catalog;\n}\n\n// A feature is a plan member when its unlock entry exists on at least one platform (even with zero actions)\nexport function isPlanMember(entry: PlatformCodes | undefined): boolean {\n if (!entry) return false;\n return PLATFORMS.some((platform) => entry[platform] !== undefined);\n}\n\n/**\n * Whether a feature's declared API surfaces admit a caller's surface.\n *\n * Lenient only about the caller: resolving without a surface (cloud's matrix builders, UI buckets)\n * filters nothing. The declared list is always strict — including `[]`, which admits no surface.\n */\nexport function surfaceAllows(surfaces: ApiSurface[], surface: ApiSurface | undefined): boolean {\n return surface === undefined || surfaces.includes(surface);\n}\n\n// The one place lock precedence is decided, for features and permissions alike; null means nothing locks.\n// Plan is the ceiling (an unentitled feature must upsell, not send the user to provision something they still\n// couldn't use), then the site deny-list, then any unprovisioned service.\nfunction resolveLockReason(\n planLocked: boolean,\n siteLocked: boolean,\n missingServices: ServiceCode[],\n): LockReason | null {\n if (planLocked) return 'PLAN';\n if (siteLocked) return 'SITE';\n if (missingServices.length > 0) return 'SERVICE';\n return null;\n}\n\n// The services a feature declares that this org has not provisioned\nfunction unmetServices(feature: SnapshotFeature, availableServices: ServiceCode[]): ServiceCode[] {\n return feature.requiredServices.filter((service) => !availableServices.includes(service));\n}\n\n// Per-platform site-lock primitive: null locks the whole feature, string[] locks those codes, absent = not locked\nexport function isSiteLockedOnPlatform(\n entry: SiteFeatureLocks[string] | undefined,\n platform: PlatformBucket,\n code: string,\n): boolean {\n const locks = entry?.[platform];\n return locks === null || (locks?.includes(code) ?? false);\n}\n\n// A feature's business-scoped permissions, each tagged with locked + reason against the plan and site deny-list\n// (bucket-scoped). Unmet services lock the whole set — an unprovisioned service blocks every action on the feature.\nfunction buildPermissions(\n feature: SnapshotFeature,\n businessCode: string,\n planMembership: PlatformCodes | undefined,\n siteLocks: SiteFeatureLocks | undefined,\n plans: Record<string, SnapshotPlan>,\n bucket: PlatformBucket,\n missingServices: ServiceCode[] = [],\n): CatalogPermission[] {\n const planUnlocked = new Set(planMembership?.[bucket] ?? []);\n const lockEntry = siteLocks?.[feature.code];\n\n // Two filters, and the second is the point: a feature reaching this surface does not mean every\n // action under it does. A code omits the bucket when no route there enforces it, so offering it\n // would promise a capability nothing can check.\n const perms = feature.permissions\n .filter((p) => p.isGlobal || p.businesses.includes(businessCode))\n .filter((p) => p.platforms.includes(bucket));\n const deps = buildDependsMap(perms);\n const codes = perms.map((p) => p.code);\n\n // Direct plan/site locks, then cascade so a locked prerequisite (e.g. view) locks its dependents (add/edit/delete)\n const directlyPlanLocked = new Set<string>();\n const directlySiteLocked = new Set<string>();\n for (const p of perms) {\n if (!planUnlocked.has(p.code)) directlyPlanLocked.add(p.code);\n if (isSiteLockedOnPlatform(lockEntry, bucket, p.code)) directlySiteLocked.add(p.code);\n }\n const directlyLocked = new Set<string>([...directlyPlanLocked, ...directlySiteLocked]);\n const lockedSet = cascadeLocked(codes, directlyLocked, deps);\n\n return perms.map((p) => {\n // A permission is enabled only if it AND its whole prerequisite closure are unlocked — reason/upsell reflect that\n const closure = [p.code, ...prereqClosure(p.code, deps)];\n const cascaded = lockedSet.has(p.code);\n const planReason = cascaded && closure.some((c) => directlyPlanLocked.has(c));\n const siteReason = cascaded && closure.some((c) => directlySiteLocked.has(c));\n const lockReason = resolveLockReason(planReason, siteReason, missingServices);\n const locked = lockReason !== null;\n const unlockPlans = lockReason === 'PLAN' ? plansUnlockingClosure(plans, feature.code, closure, bucket) : [];\n return { code: p.code, locked, lockReason, unlockPlans, missingServices };\n });\n}\n\n// Plan codes (in the business) whose unlocked set includes the permission AND its whole prerequisite closure — upsell targets\nfunction plansUnlockingClosure(\n plans: Record<string, SnapshotPlan>,\n featureCode: string,\n closure: string[],\n bucket: PlatformBucket,\n): string[] {\n const result: string[] = [];\n for (const [code, plan] of Object.entries(plans)) {\n const unlocked = plan.unlockedPermissions[featureCode]?.[bucket];\n if (unlocked && closure.every((c) => unlocked.includes(c))) result.push(code);\n }\n return result;\n}\n\n// Plan codes (in the business) that include this feature on the bucket — the feature-level upsell targets\nfunction plansIncludingFeature(\n plans: Record<string, SnapshotPlan>,\n featureCode: string,\n bucket: PlatformBucket,\n): string[] {\n const result: string[] = [];\n for (const [code, plan] of Object.entries(plans)) {\n if (plan.unlockedPermissions[featureCode]?.[bucket] !== undefined) result.push(code);\n }\n return result;\n}\n\n// The business's role templates as provisionable role items for core (identical shapes)\nexport function buildSiteRoles(snapshot: VersionSnapshot, businessCode: string | undefined): RoleItem[] {\n if (!businessCode) return [];\n const business = snapshot.businesses[businessCode];\n if (!business) return [];\n return Object.values(business.roleTemplates);\n}\n","import { type FeatureUnlocks, PLATFORMS, type PlatformCodes, type PlatformDenyCodes } from './types';\n\nexport type RevokedGrants = Record<string, PlatformDenyCodes>;\n\nexport interface ComposeRoleGrantsParams {\n baseFeatures: FeatureUnlocks | undefined;\n additions: FeatureUnlocks;\n revoked: RevokedGrants | undefined;\n}\n\n// Deduped union of two optional code lists — undefined on both sides means no platform membership\nfunction unionBucket(base: string[] | undefined, add: string[] | undefined): string[] | undefined {\n if (base === undefined && add === undefined) return undefined;\n return [...new Set([...(base ?? []), ...(add ?? [])])];\n}\n\n// Composes a custom role's effective grants: merge(base ∪ additions) − revoked (design doc §10); inputs are never mutated\nexport function composeRoleGrants(params: ComposeRoleGrantsParams): FeatureUnlocks {\n const { baseFeatures, additions, revoked } = params;\n\n const result: FeatureUnlocks = {};\n const featureCodes = new Set([...Object.keys(baseFeatures ?? {}), ...Object.keys(additions)]);\n\n for (const code of featureCodes) {\n const base = baseFeatures?.[code] ?? {};\n const add = additions[code] ?? {};\n const revokes = revoked?.[code];\n\n const composed: PlatformCodes = {};\n for (const bucket of PLATFORMS) {\n const merged = unionBucket(base[bucket], add[bucket]);\n if (merged === undefined) continue;\n const revoke = revokes?.[bucket];\n // null revokes the whole platform (membership + all codes); string[] subtracts codes but keeps membership\n if (revoke === null) continue;\n composed[bucket] = revoke === undefined ? merged : merged.filter((c) => !revoke.includes(c));\n }\n\n // A feature with no surviving platform membership disappears from the effective set.\n // Iterates PLATFORMS rather than naming buckets — the web/mobile-only version silently\n // dropped a grant surviving only on an API bucket.\n if (PLATFORMS.every((bucket) => composed[bucket] === undefined)) continue;\n result[code] = composed;\n }\n\n return result;\n}\n","import { buildSiteCatalog, findFeatureByCode } from './catalog.builder';\nimport { buildDependsMap, filterGrantedByDeps } from './permission-deps';\nimport type {\n FeatureUnlocks,\n LockReason,\n PlatformBucket,\n ScopeType,\n ServiceCode,\n SiteFeatureLocks,\n SiteType,\n VersionSnapshot,\n} from './types';\nimport { isApiBucket, snapshotFeatureKey } from './types';\n\n/**\n * The caller's surface, as the caller reports it.\n *\n * Finer than `PlatformBucket` on the mobile side — `ios` and `android` load different remote\n * entries but share one grant bucket. The API platforms are one-to-one with their buckets: an\n * API client has no variants because it has no UI.\n */\nexport type ClientPlatform = 'web' | 'ios' | 'android' | 'graphql' | 'http';\n\n// Exhaustive by type, so adding a ClientPlatform without deciding its bucket fails the build instead\n// of silently falling through to mobile — which is how an API caller would end up resolving a UI bucket.\nconst BUCKET_BY_CLIENT: Record<ClientPlatform, PlatformBucket> = {\n web: 'web',\n ios: 'mobile',\n android: 'mobile',\n graphql: 'graphql',\n http: 'http',\n};\n\n/**\n * Stands in for the microfrontend an API client does not load.\n *\n * `PermissionFeature.route` is non-optional and read by the web sidebar and the mobile host to\n * mount a remote. Nothing on the API paths reads it — the permission interceptor uses `code`,\n * `permissions` and `locked` — so an empty route keeps one shape for every bucket instead of\n * widening the field to null across every consumer.\n */\nconst EMPTY_ROUTE = { remoteEntry: '', exposedModule: '', routePrefix: '' };\n\nexport interface LockedPermission {\n code: string;\n reason: LockReason | null;\n unlockPlans: string[];\n missingServices: ServiceCode[];\n}\n\nexport interface PlanUpsell {\n plan: string;\n features: string[];\n}\n\nexport interface PermissionFeature {\n code: string;\n name: string;\n lucideIcon: string | null;\n sfSymbol: string;\n materialSymbol: string;\n permissions: string[];\n locked: boolean;\n lockReason: LockReason | null;\n unlockPlans: string[];\n // Which declared services the org has not provisioned — empty unless lockReason is 'SERVICE'\n missingServices: ServiceCode[];\n lockedPermissions: LockedPermission[];\n upsell: PlanUpsell[];\n route: {\n remoteEntry: string;\n exposedModule: string;\n routePrefix: string;\n };\n appCode: string;\n appName: string;\n appIcon: string | null;\n appSortOrder: number;\n}\n\nexport interface ResolveUserFeaturesParams {\n snapshot: VersionSnapshot;\n businessCode: string;\n planCode: string | undefined;\n siteLocks: SiteFeatureLocks | undefined;\n roleFeatures: FeatureUnlocks;\n platform: ClientPlatform;\n siteType?: SiteType;\n scope?: ScopeType;\n // External services the org has provisioned; omitting it locks every service-dependent feature\n availableServices?: ServiceCode[];\n}\n\n// Resolves the features + MF config a user sees at a BU: plan ∧ BU catalog intersected with the role's grants, filtered to the requested platform\nexport function resolveUserFeatures(params: ResolveUserFeaturesParams): PermissionFeature[] {\n const { snapshot, businessCode, planCode, siteLocks, platform, siteType, scope, availableServices } = params;\n\n // Plan unlocks, BU locks, and role grants are stored per platform; resolve only the requesting\n // surface's bucket (web → web; ios/android → mobile; graphql/http → themselves)\n const bucket: PlatformBucket = BUCKET_BY_CLIENT[platform];\n\n const roleFeatures = params.roleFeatures;\n\n // Grants/plans/locks key features by bare code; resolve to the workspace scope's variant (or any variant when unscoped)\n const featureByCode = (code: string) =>\n scope ? snapshot.features[snapshotFeatureKey(code, scope)] : findFeatureByCode(snapshot, code);\n\n // Plan ∧ BU overlay for this bucket, filtered to features that apply to this workspace scope and node type — emits EVERY applicable business feature (plan non-members come out fully locked)\n const catalog = buildSiteCatalog(\n snapshot,\n businessCode,\n planCode,\n siteLocks,\n bucket,\n siteType,\n scope,\n availableServices,\n );\n const catalogMap = new Map(catalog.map((f) => [f.code, f]));\n\n // Per-plan feature-name delta vs the current plan — feeds the plan-locked upsell screen\n const businessPlans = snapshot.businesses[businessCode]?.plans ?? {};\n const currentUnlockedCodes = new Set<string>();\n if (planCode && businessPlans[planCode]) {\n for (const [featureCode, platforms] of Object.entries(businessPlans[planCode].unlockedPermissions)) {\n if (platforms[bucket] !== undefined) currentUnlockedCodes.add(featureCode);\n }\n }\n const planAdds = new Map<string, Array<{ code: string; name: string }>>();\n for (const [planKey, plan] of Object.entries(businessPlans)) {\n if (planKey === planCode) continue;\n const adds: Array<{ code: string; name: string }> = [];\n for (const [featureCode, platforms] of Object.entries(plan.unlockedPermissions)) {\n if (platforms[bucket] === undefined || currentUnlockedCodes.has(featureCode)) continue;\n const name = featureByCode(featureCode)?.name;\n if (name) adds.push({ code: featureCode, name });\n }\n planAdds.set(planKey, adds);\n }\n\n // Granted permission set per feature, taking only this platform's grants\n const grantedFeatures = new Map<string, Set<string>>();\n for (const [code, grant] of Object.entries(roleFeatures)) {\n // Membership is the gate: undefined = not a member on this platform; [] = member with no actions (view-only)\n const granted = grant[bucket];\n if (granted === undefined) continue;\n if (!grantedFeatures.has(code)) grantedFeatures.set(code, new Set());\n for (const perm of granted) grantedFeatures.get(code)?.add(perm);\n }\n\n // Cross-reference the granted features with the catalog to build the response\n const features: PermissionFeature[] = [];\n for (const [code, permsSet] of grantedFeatures) {\n const catalogEntry = catalogMap.get(code);\n if (!catalogEntry) continue;\n\n // A UI bucket reaches its feature by loading a microfrontend, so a feature not published to\n // this platform is omitted rather than handed over as an unloadable tile. An API client loads\n // nothing — requiring a route there would make every headless feature permanently ungrantable.\n const route = isApiBucket(bucket) ? EMPTY_ROUTE : pickRouteForPlatform(catalogEntry, platform);\n if (!route) continue;\n\n // Drop granted permissions whose intra-feature prerequisites aren't also granted (e.g. add needs view)\n const featureDeps = buildDependsMap(featureByCode(code)?.permissions ?? []);\n // Plan/BU lock a subset of permissions; surface which GRANTED ones are locked + why + how to unlock (upsell)\n const permByCode = new Map(catalogEntry.permissions.map((p) => [p.code, p]));\n // Intersected with the catalog, which now omits codes this surface does not implement. Without\n // this a grant made before the flags existed — or written straight through the API — would keep\n // resolving on a bucket where no route enforces it. The picker filtering alone is cosmetic; this\n // is what makes an unimplemented grant genuinely inert.\n const grantedPerms = [...filterGrantedByDeps(permsSet, featureDeps)].filter((c) => permByCode.has(c));\n const lockedPermissions: LockedPermission[] = grantedPerms\n .map((c) => permByCode.get(c))\n .filter((p): p is NonNullable<typeof p> => !!p?.locked)\n .map((p) => ({\n code: p.code,\n reason: p.lockReason ?? null,\n unlockPlans: p.unlockPlans,\n missingServices: p.missingServices,\n }));\n\n // For a plan-locked feature, list the extra features each unlocking plan would add (excluding this feature)\n const upsell: PlanUpsell[] =\n catalogEntry.locked && catalogEntry.lockReason === 'PLAN'\n ? catalogEntry.unlockPlans\n .map((plan) => ({\n plan,\n features: (planAdds.get(plan) ?? []).filter((f) => f.code !== code).map((f) => f.name),\n }))\n .filter((group) => group.features.length > 0)\n : [];\n\n features.push({\n code,\n name: catalogEntry.name,\n lucideIcon: catalogEntry.lucideIcon,\n sfSymbol: catalogEntry.sfSymbol,\n materialSymbol: catalogEntry.materialSymbol,\n permissions: grantedPerms,\n locked: catalogEntry.locked ?? false,\n lockReason: catalogEntry.lockReason ?? null,\n unlockPlans: catalogEntry.unlockPlans,\n missingServices: catalogEntry.missingServices,\n lockedPermissions,\n upsell,\n route,\n appCode: catalogEntry.appCode,\n appName: catalogEntry.appName,\n appIcon: catalogEntry.appIcon,\n appSortOrder: catalogEntry.appSortOrder,\n });\n }\n\n // Order app-alphabetically so the core-web sidebar (groups by app) renders apps sorted without any frontend re-sort;\n // stable sort keeps each app's features in their existing relative order\n features.sort((a, b) => a.appName.localeCompare(b.appName));\n\n return features;\n}\n\n// Selects the route block from a catalog entry for the requested platform, or null when it doesn't publish there\nexport function pickRouteForPlatform(\n entry: {\n web: {\n remoteEntry: string;\n exposedModule: string;\n routePrefix: string;\n } | null;\n mobile: {\n remoteEntryAndroid: string;\n remoteEntryIos: string;\n exposedModule: string;\n routePrefix: string;\n } | null;\n },\n platform: ClientPlatform,\n): { remoteEntry: string; exposedModule: string; routePrefix: string } | null {\n // API platforms load nothing — resolveUserFeatures never routes them here, and answering with the\n // web block for an unhandled value would hand an API caller a remote it cannot mount\n if (platform === 'graphql' || platform === 'http') return null;\n if (platform === 'ios' || platform === 'android') {\n if (!entry.mobile) return null;\n return {\n remoteEntry: platform === 'ios' ? entry.mobile.remoteEntryIos : entry.mobile.remoteEntryAndroid,\n exposedModule: entry.mobile.exposedModule,\n routePrefix: entry.mobile.routePrefix,\n };\n }\n // Web\n if (!entry.web) return null;\n return {\n remoteEntry: entry.web.remoteEntry,\n exposedModule: entry.web.exposedModule,\n routePrefix: entry.web.routePrefix,\n };\n}\n","import { featureAppliesAtNode, isPlanMember, isSiteLockedOnPlatform } from './catalog.builder';\nimport {\n API_BUCKETS,\n type ApiSurface,\n type PlatformBucket,\n type ScopeType,\n type SiteFeatureLocks,\n type SiteType,\n type SnapshotPlan,\n SURFACE_BY_BUCKET,\n snapshotFeatureKey,\n UI_PLATFORMS,\n type VersionSnapshot,\n} from './types';\n\nexport interface SiteMatrixCell {\n inPlan: boolean;\n selected: boolean;\n availableIn: string[];\n}\n\nexport interface SiteMatrixPermission {\n code: string;\n label: string;\n dependsOn: string[];\n web: SiteMatrixCell | null;\n mobile: SiteMatrixCell | null;\n graphql: SiteMatrixCell | null;\n http: SiteMatrixCell | null;\n}\n\nexport interface SiteMatrixFeature {\n code: string;\n name: string;\n icon: string | null;\n scope: ScopeType;\n applicableSiteTypes: SiteType[];\n platforms: PlatformBucket[];\n inPlan: boolean;\n availableIn: string[];\n // The API surfaces the feature declares — what lets the app-credential editor filter by the\n // credential's type.\n apiSurfaces: ApiSurface[];\n permissions: SiteMatrixPermission[];\n}\n\nexport interface MatrixCounts {\n unlocked: number;\n total: number;\n}\n\nexport interface SiteMatrixApp {\n code: string;\n name: string;\n icon: string | null;\n // Counted per surface, not as one total: a consumer showing a subset of the columns (the\n // app-credential editor shows exactly one) sums the surfaces it renders. One number covering all\n // four read as \"20/20 unlocked\" above the 5 checkboxes actually on screen.\n counts: Record<PlatformBucket, MatrixCounts>;\n features: SiteMatrixFeature[];\n}\n\nexport interface SiteMatrix {\n plan: { code: string; name: string };\n apps: SiteMatrixApp[];\n locks: SiteFeatureLocks;\n}\n\n// Builds the SITE-only apps/features/permissions matrix — not filtered to plan members; plan-locked items carry inPlan=false + availableIn\nexport function buildSiteMatrix(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n siteLocks: SiteFeatureLocks | undefined,\n siteType?: SiteType,\n): SiteMatrix {\n return buildMatrix(snapshot, businessCode, planCode, siteLocks, false, siteType);\n}\n\n// Builds the all-scopes apps/features/permissions matrix — every scope's features included, each carrying its real scope; powers the Plan Overview + Create Custom Role picker\nexport function buildPlanMatrix(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n siteLocks?: SiteFeatureLocks,\n): SiteMatrix {\n return buildMatrix(snapshot, businessCode, planCode, siteLocks, true);\n}\n\n// Shared matrix builder — allScopes=false keeps only SITE refs; allScopes=true includes every scope and emits each feature's real scope\nfunction buildMatrix(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n siteLocks: SiteFeatureLocks | undefined,\n allScopes: boolean,\n siteType?: SiteType,\n): SiteMatrix {\n const business = businessCode ? snapshot.businesses[businessCode] : undefined;\n const plans = business?.plans ?? {};\n const plan = planCode ? plans[planCode] : undefined;\n const planMeta = { code: planCode ?? '', name: plan?.name ?? planCode ?? '' };\n const locks = siteLocks ?? {};\n if (!business || !plan) return { plan: planMeta, apps: [], locks };\n\n const apps: SiteMatrixApp[] = [];\n for (const app of business.apps) {\n const counts: Record<PlatformBucket, MatrixCounts> = {\n web: { unlocked: 0, total: 0 },\n mobile: { unlocked: 0, total: 0 },\n graphql: { unlocked: 0, total: 0 },\n http: { unlocked: 0, total: 0 },\n };\n const features: SiteMatrixFeature[] = [];\n\n for (const ref of app.features) {\n if (!allScopes && ref.scope !== 'SITE') continue;\n const code = ref.code;\n const feature = snapshot.features[snapshotFeatureKey(code, ref.scope)];\n if (!feature) continue;\n if (siteType !== undefined && !featureAppliesAtNode(feature.applicableSiteTypes, siteType)) continue;\n // A UI bucket is offered only where the feature publishes a microfrontend; each API bucket is\n // offered where the feature declares its surface. An undeclared surface shows an em dash like\n // a missing microfrontend does.\n const platforms: PlatformBucket[] = [\n ...UI_PLATFORMS.filter((p) => !!feature.microfrontends?.[p]),\n ...API_BUCKETS.filter((b) => feature.apiSurfaces.includes(SURFACE_BY_BUCKET[b])),\n ];\n\n const groupByCode = new Map(feature.permissionGroups.map((g) => [g.code, g]));\n const membership = plan.unlockedPermissions[code];\n const featureInPlan = isPlanMember(membership);\n const siteEntry = siteLocks?.[code];\n\n const permissions: SiteMatrixPermission[] = feature.permissions\n .filter((p) => p.isGlobal || p.businesses.includes(businessCode ?? ''))\n .map((p) => {\n const cell = (plat: PlatformBucket): SiteMatrixCell | null => {\n // The feature must reach this bucket AND this code must be implemented on it — the same\n // two gates buildSiteCatalog applies, so the matrix and the catalog cannot disagree\n if (!platforms.includes(plat) || !p.platforms.includes(plat)) return null;\n const planCodes = membership?.[plat];\n const inPlan = featureInPlan && planCodes !== undefined && planCodes.includes(p.code);\n // Deny-list: an in-plan cell is selected unless the site locks it on this platform\n const selected = inPlan && !isSiteLockedOnPlatform(siteEntry, plat, p.code);\n const availableIn = inPlan ? [] : plansUnlockingPerm(plans, code, p.code, plat, planCode);\n counts[plat].total += 1;\n if (inPlan) counts[plat].unlocked += 1;\n return { inPlan, selected, availableIn };\n };\n return {\n code: p.code,\n label: p.label,\n dependsOn: p.dependsOn,\n group: p.group ? groupByCode.get(p.group) : undefined,\n web: cell('web'),\n mobile: cell('mobile'),\n graphql: cell('graphql'),\n http: cell('http'),\n };\n });\n\n features.push({\n code: feature.code,\n name: feature.name,\n icon: feature.lucideIcon ?? null,\n scope: feature.scope,\n applicableSiteTypes: feature.applicableSiteTypes,\n platforms,\n inPlan: featureInPlan,\n availableIn: featureInPlan ? [] : plansIncludingFeature(plans, code, planCode),\n apiSurfaces: feature.apiSurfaces,\n permissions,\n });\n }\n\n if (features.length === 0) continue;\n apps.push({ code: app.code, name: app.name, icon: app.icon ?? null, counts, features });\n }\n\n // Emit apps alphabetically by name so every consumer (Plan Overview, Role picker, all Locks screens) renders them sorted\n apps.sort((a, b) => a.name.localeCompare(b.name));\n\n return { plan: planMeta, apps, locks };\n}\n\n// Names of other plans (excluding the org's own) that unlock this feature+permission on the given platform\nfunction plansUnlockingPerm(\n plans: Record<string, SnapshotPlan>,\n featureCode: string,\n permCode: string,\n platform: PlatformBucket,\n excludeCode: string | undefined,\n): string[] {\n const names: string[] = [];\n for (const [code, p] of Object.entries(plans)) {\n if (code === excludeCode) continue;\n if ((p.unlockedPermissions[featureCode]?.[platform] ?? []).includes(permCode)) names.push(p.name);\n }\n return names;\n}\n\n// Names of other plans (excluding the org's own) that include this feature at all (membership) — feature-level upsell\nfunction plansIncludingFeature(\n plans: Record<string, SnapshotPlan>,\n featureCode: string,\n excludeCode: string | undefined,\n): string[] {\n const names: string[] = [];\n for (const [code, p] of Object.entries(plans)) {\n if (code === excludeCode) continue;\n if (isPlanMember(p.unlockedPermissions[featureCode])) names.push(p.name);\n }\n return names;\n}\n"],"mappings":"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","names":["buildDependsMap","permissions","present","Set","map","p","code","Map","set","dependsOn","filter","dep","has","prereqClosure","deps","out","seen","stack","length","current","pop","get","add","push","cascadeLocked","codes","directlyLocked","locked","visiting","check","viaDep","some","delete","filterGrantedByDeps","granted","ok","satisfied","every","PLATFORMS","UI_PLATFORMS","API_SURFACES","API_BUCKETS","SURFACE_BY_BUCKET","graphql","http","BUCKET_BY_SURFACE","GRAPHQL","HTTP","isApiBucket","bucket","SITE_TYPES","SERVICE_CODES","snapshotFeatureKey","code","scope","SNAPSHOT_SCHEMA_VERSION","featureAppliesAtNode","applicableSiteTypes","siteType","includes","findFeatureByCode","snapshot","code","feature","Object","values","features","undefined","buildSiteCatalog","businessCode","planCode","siteLocks","bucket","scope","availableServices","business","businesses","plans","plan","locks","catalog","sortedApps","apps","sort","a","b","name","localeCompare","app","businessAppFeatures","filter","ref","map","snapshotFeatureKey","f","isApiBucket","surfaceAllows","apiSurfaces","SURFACE_BY_BUCKET","microfrontends","web","mobile","length","membership","unlockedPermissions","memberOnBucket","sitePlatformLocked","missingServices","unmetServices","permissions","buildPermissions","lockReason","resolveLockReason","locked","unlockPlans","plansIncludingFeature","push","lucideIcon","sfSymbol","materialSymbol","remoteEntry","exposedModule","routePrefix","remoteEntryAndroid","remoteEntryIos","appCode","appName","appIcon","icon","appSortOrder","sortOrder","isPlanMember","entry","PLATFORMS","some","platform","surfaces","surface","planLocked","siteLocked","requiredServices","service","isSiteLockedOnPlatform","planMembership","planUnlocked","Set","lockEntry","perms","p","isGlobal","platforms","deps","buildDependsMap","codes","directlyPlanLocked","directlySiteLocked","has","add","directlyLocked","lockedSet","cascadeLocked","closure","prereqClosure","cascaded","planReason","c","siteReason","plansUnlockingClosure","featureCode","result","entries","unlocked","every","buildSiteRoles","roleTemplates","unionBucket","base","add","undefined","Set","composeRoleGrants","params","baseFeatures","additions","revoked","result","featureCodes","Object","keys","code","revokes","composed","bucket","PLATFORMS","merged","revoke","filter","c","includes","every","BUCKET_BY_CLIENT","web","ios","android","graphql","http","EMPTY_ROUTE","remoteEntry","exposedModule","routePrefix","resolveUserFeatures","params","snapshot","businessCode","planCode","siteLocks","platform","siteType","scope","availableServices","bucket","roleFeatures","featureByCode","code","features","snapshotFeatureKey","findFeatureByCode","catalog","buildSiteCatalog","catalogMap","Map","map","f","businessPlans","businesses","plans","currentUnlockedCodes","Set","featureCode","platforms","Object","entries","unlockedPermissions","undefined","add","planAdds","planKey","plan","adds","has","name","push","set","grantedFeatures","grant","granted","perm","get","permsSet","catalogEntry","route","isApiBucket","pickRouteForPlatform","featureDeps","buildDependsMap","permissions","permByCode","p","grantedPerms","filterGrantedByDeps","filter","c","lockedPermissions","locked","reason","lockReason","unlockPlans","missingServices","upsell","group","length","lucideIcon","sfSymbol","materialSymbol","appCode","appName","appIcon","appSortOrder","sort","a","b","localeCompare","entry","mobile","remoteEntryIos","remoteEntryAndroid","buildSiteMatrix","snapshot","businessCode","planCode","siteLocks","siteType","buildMatrix","buildPlanMatrix","allScopes","business","businesses","undefined","plans","plan","planMeta","code","name","locks","apps","app","counts","web","unlocked","total","mobile","graphql","http","features","ref","scope","feature","snapshotFeatureKey","featureAppliesAtNode","applicableSiteTypes","platforms","UI_PLATFORMS","filter","p","microfrontends","API_BUCKETS","b","apiSurfaces","includes","SURFACE_BY_BUCKET","groupByCode","Map","permissionGroups","map","g","membership","unlockedPermissions","featureInPlan","isPlanMember","siteEntry","permissions","isGlobal","cell","plat","planCodes","inPlan","selected","isSiteLockedOnPlatform","availableIn","plansUnlockingPerm","label","dependsOn","group","get","push","icon","lucideIcon","plansIncludingFeature","length","sort","a","localeCompare","featureCode","permCode","platform","excludeCode","names","Object","entries"]}
|
|
1
|
+
{"version":3,"sources":["../src/catalog-resolver/permission-deps.ts","../src/catalog-resolver/types.ts","../src/catalog-resolver/catalog.builder.ts","../src/catalog-resolver/compose-role-grants.ts","../src/catalog-resolver/resolve-user-features.ts","../src/catalog-resolver/site-matrix.builder.ts"],"sourcesContent":["// Intra-feature permission prerequisites — only DIRECT edges are declared; the transitive closure is computed by recursion, cycle-guarded\n\nexport type DependsMap = Map<string, string[]>;\n\n// Builds a dependency map from a feature's permissions, keeping only edges to codes present in the set\nexport function buildDependsMap(permissions: Array<{ code: string; dependsOn?: string[] }>): DependsMap {\n const present = new Set(permissions.map((p) => p.code));\n const map: DependsMap = new Map();\n for (const p of permissions) {\n map.set(\n p.code,\n (p.dependsOn ?? []).filter((dep) => dep !== p.code && present.has(dep)),\n );\n }\n return map;\n}\n\n// Transitive prerequisite closure of a code (excludes the code itself), cycle-safe\nexport function prereqClosure(code: string, deps: DependsMap): string[] {\n const out = new Set<string>();\n const seen = new Set<string>([code]);\n const stack = [code];\n while (stack.length > 0) {\n const current = stack.pop() as string;\n for (const dep of deps.get(current) ?? []) {\n if (seen.has(dep)) continue;\n seen.add(dep);\n out.add(dep);\n stack.push(dep);\n }\n }\n return [...out];\n}\n\n// Codes locked after cascade: a code is locked if directly locked or any transitive prerequisite is (cycle-safe)\nexport function cascadeLocked(codes: string[], directlyLocked: Set<string>, deps: DependsMap): Set<string> {\n const locked = new Set<string>();\n const visiting = new Set<string>();\n const check = (code: string): boolean => {\n if (locked.has(code)) return true;\n if (directlyLocked.has(code)) {\n locked.add(code);\n return true;\n }\n if (visiting.has(code)) return false;\n visiting.add(code);\n const viaDep = (deps.get(code) ?? []).some(check);\n visiting.delete(code);\n if (viaDep) locked.add(code);\n return viaDep;\n };\n for (const code of codes) check(code);\n return locked;\n}\n\n// Keeps only codes whose FULL prerequisite closure is also present — drops a dependent missing any prerequisite (cycle-safe)\nexport function filterGrantedByDeps(granted: Set<string>, deps: DependsMap): Set<string> {\n const ok = new Set<string>();\n const visiting = new Set<string>();\n const check = (code: string): boolean => {\n if (ok.has(code)) return true;\n if (!granted.has(code)) return false;\n if (visiting.has(code)) return true;\n visiting.add(code);\n const satisfied = (deps.get(code) ?? []).every(check);\n visiting.delete(code);\n if (satisfied) ok.add(code);\n return satisfied;\n };\n for (const code of granted) check(code);\n return ok;\n}\n","// ——— Platform algebra — plan unlocks, role grants, and BU locks are all stored per platform bucket ———\n\n/**\n * The surfaces a permission can be granted on.\n *\n * `web` and `mobile` are UI buckets: a feature reaches them through a microfrontend, and a grant\n * there means a person can operate it on that surface. `graphql` and `http` are not UIs at all —\n * each is an API surface a credential signs its own requests against, so they have no\n * microfrontend and no route, and a feature needs neither to be reachable on one.\n *\n * Keeping the API buckets in the same algebra rather than beside it is what lets plan entitlement,\n * node feature locks and permission prerequisites bind an API client exactly as they bind a person.\n * One bucket per surface is what lets a plan entitle GraphQL and HTTP access independently.\n */\nexport type PlatformBucket = 'web' | 'mobile' | 'graphql' | 'http';\n\nexport const PLATFORMS: PlatformBucket[] = ['web', 'mobile', 'graphql', 'http'];\n\n/** Buckets that reach their feature through a microfrontend, and so require one to resolve. */\nexport type UiPlatformBucket = 'web' | 'mobile';\n\nexport const UI_PLATFORMS: UiPlatformBucket[] = ['web', 'mobile'];\n\n// The API surfaces an app credential can present — literally the values of core's `app_type` enum, so\n// enforcement is a plain lookup with no mapping. A feature declares which surfaces expose it.\nexport const API_SURFACES = ['GRAPHQL', 'HTTP'] as const;\nexport type ApiSurface = (typeof API_SURFACES)[number];\n\n/** Buckets that admit an API credential rather than a person — exactly one per surface. */\nexport type ApiBucket = Exclude<PlatformBucket, UiPlatformBucket>;\n\nexport const API_BUCKETS: ApiBucket[] = ['graphql', 'http'];\n\nexport const SURFACE_BY_BUCKET: Record<ApiBucket, ApiSurface> = { graphql: 'GRAPHQL', http: 'HTTP' };\nexport const BUCKET_BY_SURFACE: Record<ApiSurface, ApiBucket> = { GRAPHQL: 'graphql', HTTP: 'http' };\n\nexport function isApiBucket(bucket: PlatformBucket): bucket is ApiBucket {\n return bucket === 'graphql' || bucket === 'http';\n}\n\nexport interface PlatformCodes {\n web?: string[];\n mobile?: string[];\n graphql?: string[];\n http?: string[];\n}\n\nexport interface PlatformDenyCodes {\n web?: string[] | null;\n mobile?: string[] | null;\n graphql?: string[] | null;\n http?: string[] | null;\n}\n\nexport type FeatureUnlocks = Record<string, PlatformCodes>;\n\nexport type FeatureLocks = Record<string, PlatformDenyCodes>;\nexport type SiteFeatureLocks = FeatureLocks;\n\n// ——— Snapshot document shape — what gets stored in versions.snapshot and signed into the catalog license ———\n\nexport interface PermissionGroupRef {\n code: string;\n label: string;\n sortOrder: number;\n}\n\nexport interface SnapshotPermission {\n code: string;\n label: string;\n isGlobal: boolean;\n businesses: string[];\n dependsOn: string[];\n platforms: PlatformBucket[];\n // Code of the group this action sits under, resolved against the feature's `permissionGroups`.\n // Absent on a feature's own actions, which head the list under no heading.\n group?: string;\n}\nexport interface SnapshotMicrofrontendWeb {\n code: string;\n name: string;\n remoteEntry: string;\n exposedModule: string;\n routePrefix: string;\n}\nexport interface SnapshotMicrofrontendMobile {\n code: string;\n name: string;\n remoteEntryAndroid: string;\n remoteEntryIos: string;\n exposedModule: string;\n routePrefix: string;\n}\nexport interface SnapshotMicrofrontends {\n web?: SnapshotMicrofrontendWeb;\n mobile?: SnapshotMicrofrontendMobile;\n}\nexport type ScopeType = 'ORG' | 'LE' | 'SITE_GROUP' | 'SITE';\nexport type SiteType = 'OUTLET' | 'WAREHOUSE' | 'PRODUCTION';\nexport const SITE_TYPES: SiteType[] = ['OUTLET', 'WAREHOUSE', 'PRODUCTION'];\n// External services a feature can depend on — the org must have the service provisioned before the feature\n// unlocks. Add new services here and nowhere else in this package; every lock path is service-agnostic.\nexport const SERVICE_CODES = ['GITEA'] as const;\nexport type ServiceCode = (typeof SERVICE_CODES)[number];\nexport interface SnapshotFeature {\n code: string;\n name: string;\n lucideIcon: string;\n sfSymbol: string;\n materialSymbol: string;\n scope: ScopeType;\n applicableSiteTypes: SiteType[];\n permissions: SnapshotPermission[];\n microfrontends: SnapshotMicrofrontends;\n requiredServices: ServiceCode[];\n // The feature's sub-resources, carried once rather than repeated on each of their permissions\n permissionGroups: PermissionGroupRef[];\n // Strict — it decides which of the `graphql`/`http` buckets the feature offers at all, and `[]` offers neither\n apiSurfaces: ApiSurface[];\n}\nexport interface SnapshotAppFeatureRef {\n code: string;\n scope: ScopeType;\n}\nexport interface SnapshotApp {\n code: string;\n name: string;\n icon: string;\n sortOrder: number;\n features: SnapshotAppFeatureRef[];\n}\nexport interface SnapshotRoleTemplate {\n name: string;\n code: string;\n scope: ScopeType;\n siteType: SiteType;\n features: FeatureUnlocks;\n}\nexport interface SnapshotPlan {\n code: string;\n name: string;\n isCustom: boolean;\n maxSites: number | null;\n unlockedPermissions: FeatureUnlocks;\n}\nexport interface VocabularyEntry {\n singular: string;\n plural: string;\n}\nexport interface BusinessVocabulary {\n site?: VocabularyEntry;\n siteGroup?: VocabularyEntry;\n outlet?: VocabularyEntry;\n warehouse?: VocabularyEntry;\n production?: VocabularyEntry;\n}\nexport interface SnapshotBusiness {\n name: string;\n vocabulary?: BusinessVocabulary;\n roleTemplates: Record<string, SnapshotRoleTemplate>;\n plans: Record<string, SnapshotPlan>;\n}\nexport interface VersionSnapshot {\n schemaVersion?: number;\n // Flat feature dictionary keyed by `${scope}.${code}` (see snapshotFeatureKey) — same-code features at different scopes stay distinct\n features: Record<string, SnapshotFeature>;\n apps: SnapshotApp[];\n businesses: Record<string, SnapshotBusiness>;\n}\n\n// Composite key for the snapshot feature dictionary — feature identity is (scope, code)\nexport function snapshotFeatureKey(code: string, scope: ScopeType): string {\n return `${scope}.${code}`;\n}\n\nexport const SNAPSHOT_SCHEMA_VERSION = 6;\n\n// SERVICE = the org has not provisioned an external service the feature declares; the specific services are\n// reported alongside in `missingServices` so callers never branch on a service code baked into this union\nexport type LockReason = 'PLAN' | 'SITE' | 'SERVICE';\n\nexport interface CatalogPermission {\n code: string;\n locked: boolean;\n lockReason: LockReason | null;\n unlockPlans: string[];\n missingServices: ServiceCode[];\n}\n\nexport interface FeatureCatalogEntry {\n code: string;\n name: string;\n lucideIcon: string | null;\n sfSymbol: string;\n materialSymbol: string;\n web: {\n remoteEntry: string;\n exposedModule: string;\n routePrefix: string;\n } | null;\n mobile: {\n remoteEntryAndroid: string;\n remoteEntryIos: string;\n exposedModule: string;\n routePrefix: string;\n } | null;\n appCode: string;\n appName: string;\n appIcon: string | null;\n appSortOrder: number;\n locked: boolean;\n lockReason: LockReason | null;\n unlockPlans: string[];\n missingServices: ServiceCode[];\n permissions: CatalogPermission[];\n}\n\nexport type RoleItem = SnapshotRoleTemplate;\n","import { buildDependsMap, cascadeLocked, prereqClosure } from './permission-deps';\nimport type {\n ApiSurface,\n CatalogPermission,\n FeatureCatalogEntry,\n LockReason,\n PlatformBucket,\n PlatformCodes,\n RoleItem,\n ScopeType,\n ServiceCode,\n SiteFeatureLocks,\n SiteType,\n SnapshotFeature,\n SnapshotPlan,\n VersionSnapshot,\n} from './types';\nimport { isApiBucket, PLATFORMS, SURFACE_BY_BUCKET, snapshotFeatureKey } from './types';\n\n// Whether a feature with the given site-type applicability is exposed at this site type\nexport function featureAppliesAtNode(applicableSiteTypes: SiteType[], siteType: SiteType): boolean {\n return applicableSiteTypes.includes(siteType);\n}\n\n// Scope-agnostic lookup of a feature by bare code — grants/locks key features by code alone, so the first scope-variant's shared metadata (permission graph) answers\nexport function findFeatureByCode(snapshot: VersionSnapshot, code: string): SnapshotFeature | undefined {\n for (const feature of Object.values(snapshot.features)) {\n if (feature.code === code) return feature;\n }\n return undefined;\n}\n\n// Builds the per-site catalog for ONE platform bucket — plan is the ceiling, siteLocks is a deny-list within it; each permission carries locked + lockReason + unlockPlans\n// availableServices defaults to none, so a caller that doesn't know the org's provisioned services locks every service-dependent feature rather than leaking it\nexport function buildSiteCatalog(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n siteLocks: SiteFeatureLocks | undefined,\n bucket: PlatformBucket,\n siteType?: SiteType,\n scope?: ScopeType,\n availableServices: ServiceCode[] = [],\n): FeatureCatalogEntry[] {\n if (!businessCode) return [];\n const business = snapshot.businesses[businessCode];\n if (!business) return [];\n const plans = business.plans;\n const plan = planCode ? plans[planCode] : undefined;\n const locks = siteLocks;\n\n const catalog: FeatureCatalogEntry[] = [];\n // Iterate apps alphabetically by name so the resolved feature list (→ core-web sidebar) is app-alphabetical without any frontend re-sort\n const sortedApps = [...snapshot.apps].sort((a, b) => a.name.localeCompare(b.name));\n for (const app of sortedApps) {\n // The app's renderable features (each ref pins scope+code to one app), dropped when they don't belong to this workspace scope or node type (outlet vs container)\n const businessAppFeatures = app.features\n .filter((ref) => scope === undefined || ref.scope === scope)\n .map((ref) => snapshot.features[snapshotFeatureKey(ref.code, ref.scope)])\n .filter(\n (f): f is SnapshotFeature =>\n !!f &&\n // A UI bucket needs something to render, so a feature shipping no microfrontend is dropped.\n // An API bucket renders nothing — there a feature is admitted by the surfaces it declares\n // instead, so a GRAPHQL credential never resolves an HTTP-only feature. A surface-excluded\n // feature vanishes from the catalog entirely, which is what makes resolution fail closed.\n (isApiBucket(bucket)\n ? surfaceAllows(f.apiSurfaces, SURFACE_BY_BUCKET[bucket])\n : !!(f.microfrontends?.web || f.microfrontends?.mobile)) &&\n (siteType === undefined || featureAppliesAtNode(f.applicableSiteTypes, siteType)),\n );\n\n if (businessAppFeatures.length === 0) continue;\n\n // Emit EVERY business feature so a role's grant on a plan-omitted feature still resolves as a locked tile instead of vanishing\n for (const feature of businessAppFeatures) {\n const membership = plan?.unlockedPermissions[feature.code];\n // Routes are exposed wherever the feature SHIPS — membership never hides them\n const web = feature.microfrontends?.web;\n const mobile = feature.microfrontends?.mobile;\n\n // Feature-level lock is EXPLICIT: plan must include the feature on this bucket, the site must not null-lock\n // the platform, and every external service the feature declares must be provisioned for the org\n const memberOnBucket = membership?.[bucket] !== undefined;\n const sitePlatformLocked = locks?.[feature.code]?.[bucket] === null;\n const missingServices = unmetServices(feature, availableServices);\n // Unmet services lock every permission too — otherwise the feature reads locked while its actions still\n // report as available, which is not how plan and site locks behave\n const permissions = buildPermissions(feature, businessCode, membership, locks, plans, bucket, missingServices);\n const lockReason = resolveLockReason(!memberOnBucket, sitePlatformLocked, missingServices);\n const locked = lockReason !== null;\n const unlockPlans = lockReason === 'PLAN' ? plansIncludingFeature(plans, feature.code, bucket) : [];\n\n catalog.push({\n code: feature.code,\n name: feature.name,\n lucideIcon: feature.lucideIcon ?? null,\n sfSymbol: feature.sfSymbol ?? 'square',\n materialSymbol: feature.materialSymbol ?? 'square',\n web: web\n ? {\n remoteEntry: web.remoteEntry ?? '',\n exposedModule: web.exposedModule ?? '',\n routePrefix: web.routePrefix ?? '',\n }\n : null,\n mobile: mobile\n ? {\n remoteEntryAndroid: mobile.remoteEntryAndroid ?? '',\n remoteEntryIos: mobile.remoteEntryIos ?? '',\n exposedModule: mobile.exposedModule ?? '',\n routePrefix: mobile.routePrefix ?? '',\n }\n : null,\n appCode: app.code,\n appName: app.name,\n appIcon: app.icon ?? null,\n appSortOrder: app.sortOrder ?? 0,\n locked,\n lockReason,\n unlockPlans,\n missingServices,\n permissions,\n });\n }\n }\n return catalog;\n}\n\n// A feature is a plan member when its unlock entry exists on at least one platform (even with zero actions)\nexport function isPlanMember(entry: PlatformCodes | undefined): boolean {\n if (!entry) return false;\n return PLATFORMS.some((platform) => entry[platform] !== undefined);\n}\n\n/**\n * Whether a feature's declared API surfaces admit a caller's surface.\n *\n * Lenient only about the caller: resolving without a surface (cloud's matrix builders, UI buckets)\n * filters nothing. The declared list is always strict — including `[]`, which admits no surface.\n */\nexport function surfaceAllows(surfaces: ApiSurface[], surface: ApiSurface | undefined): boolean {\n return surface === undefined || surfaces.includes(surface);\n}\n\n// The one place lock precedence is decided, for features and permissions alike; null means nothing locks.\n// Plan is the ceiling (an unentitled feature must upsell, not send the user to provision something they still\n// couldn't use), then the site deny-list, then any unprovisioned service.\nfunction resolveLockReason(\n planLocked: boolean,\n siteLocked: boolean,\n missingServices: ServiceCode[],\n): LockReason | null {\n if (planLocked) return 'PLAN';\n if (siteLocked) return 'SITE';\n if (missingServices.length > 0) return 'SERVICE';\n return null;\n}\n\n// The services a feature declares that this org has not provisioned\nfunction unmetServices(feature: SnapshotFeature, availableServices: ServiceCode[]): ServiceCode[] {\n return feature.requiredServices.filter((service) => !availableServices.includes(service));\n}\n\n// Per-platform site-lock primitive: null locks the whole feature, string[] locks those codes, absent = not locked\nexport function isSiteLockedOnPlatform(\n entry: SiteFeatureLocks[string] | undefined,\n platform: PlatformBucket,\n code: string,\n): boolean {\n const locks = entry?.[platform];\n return locks === null || (locks?.includes(code) ?? false);\n}\n\n// A feature's business-scoped permissions, each tagged with locked + reason against the plan and site deny-list\n// (bucket-scoped). Unmet services lock the whole set — an unprovisioned service blocks every action on the feature.\nfunction buildPermissions(\n feature: SnapshotFeature,\n businessCode: string,\n planMembership: PlatformCodes | undefined,\n siteLocks: SiteFeatureLocks | undefined,\n plans: Record<string, SnapshotPlan>,\n bucket: PlatformBucket,\n missingServices: ServiceCode[] = [],\n): CatalogPermission[] {\n const planUnlocked = new Set(planMembership?.[bucket] ?? []);\n const lockEntry = siteLocks?.[feature.code];\n\n // Two filters, and the second is the point: a feature reaching this surface does not mean every\n // action under it does. A code omits the bucket when no route there enforces it, so offering it\n // would promise a capability nothing can check.\n const perms = feature.permissions\n .filter((p) => p.isGlobal || p.businesses.includes(businessCode))\n .filter((p) => p.platforms.includes(bucket));\n const deps = buildDependsMap(perms);\n const codes = perms.map((p) => p.code);\n\n // Direct plan/site locks, then cascade so a locked prerequisite (e.g. view) locks its dependents (add/edit/delete)\n const directlyPlanLocked = new Set<string>();\n const directlySiteLocked = new Set<string>();\n for (const p of perms) {\n if (!planUnlocked.has(p.code)) directlyPlanLocked.add(p.code);\n if (isSiteLockedOnPlatform(lockEntry, bucket, p.code)) directlySiteLocked.add(p.code);\n }\n const directlyLocked = new Set<string>([...directlyPlanLocked, ...directlySiteLocked]);\n const lockedSet = cascadeLocked(codes, directlyLocked, deps);\n\n return perms.map((p) => {\n // A permission is enabled only if it AND its whole prerequisite closure are unlocked — reason/upsell reflect that\n const closure = [p.code, ...prereqClosure(p.code, deps)];\n const cascaded = lockedSet.has(p.code);\n const planReason = cascaded && closure.some((c) => directlyPlanLocked.has(c));\n const siteReason = cascaded && closure.some((c) => directlySiteLocked.has(c));\n const lockReason = resolveLockReason(planReason, siteReason, missingServices);\n const locked = lockReason !== null;\n const unlockPlans = lockReason === 'PLAN' ? plansUnlockingClosure(plans, feature.code, closure, bucket) : [];\n return { code: p.code, locked, lockReason, unlockPlans, missingServices };\n });\n}\n\n// Plan codes (in the business) whose unlocked set includes the permission AND its whole prerequisite closure — upsell targets\nfunction plansUnlockingClosure(\n plans: Record<string, SnapshotPlan>,\n featureCode: string,\n closure: string[],\n bucket: PlatformBucket,\n): string[] {\n const result: string[] = [];\n for (const [code, plan] of Object.entries(plans)) {\n const unlocked = plan.unlockedPermissions[featureCode]?.[bucket];\n if (unlocked && closure.every((c) => unlocked.includes(c))) result.push(code);\n }\n return result;\n}\n\n// Plan codes (in the business) that include this feature on the bucket — the feature-level upsell targets\nfunction plansIncludingFeature(\n plans: Record<string, SnapshotPlan>,\n featureCode: string,\n bucket: PlatformBucket,\n): string[] {\n const result: string[] = [];\n for (const [code, plan] of Object.entries(plans)) {\n if (plan.unlockedPermissions[featureCode]?.[bucket] !== undefined) result.push(code);\n }\n return result;\n}\n\n// The business's role templates as provisionable role items for core (identical shapes)\nexport function buildSiteRoles(snapshot: VersionSnapshot, businessCode: string | undefined): RoleItem[] {\n if (!businessCode) return [];\n const business = snapshot.businesses[businessCode];\n if (!business) return [];\n return Object.values(business.roleTemplates);\n}\n","import { type FeatureUnlocks, PLATFORMS, type PlatformCodes, type PlatformDenyCodes } from './types';\n\nexport type RevokedGrants = Record<string, PlatformDenyCodes>;\n\nexport interface ComposeRoleGrantsParams {\n baseFeatures: FeatureUnlocks | undefined;\n additions: FeatureUnlocks;\n revoked: RevokedGrants | undefined;\n}\n\n// Deduped union of two optional code lists — undefined on both sides means no platform membership\nfunction unionBucket(base: string[] | undefined, add: string[] | undefined): string[] | undefined {\n if (base === undefined && add === undefined) return undefined;\n return [...new Set([...(base ?? []), ...(add ?? [])])];\n}\n\n// Composes a custom role's effective grants: merge(base ∪ additions) − revoked (design doc §10); inputs are never mutated\nexport function composeRoleGrants(params: ComposeRoleGrantsParams): FeatureUnlocks {\n const { baseFeatures, additions, revoked } = params;\n\n const result: FeatureUnlocks = {};\n const featureCodes = new Set([...Object.keys(baseFeatures ?? {}), ...Object.keys(additions)]);\n\n for (const code of featureCodes) {\n const base = baseFeatures?.[code] ?? {};\n const add = additions[code] ?? {};\n const revokes = revoked?.[code];\n\n const composed: PlatformCodes = {};\n for (const bucket of PLATFORMS) {\n const merged = unionBucket(base[bucket], add[bucket]);\n if (merged === undefined) continue;\n const revoke = revokes?.[bucket];\n // null revokes the whole platform (membership + all codes); string[] subtracts codes but keeps membership\n if (revoke === null) continue;\n composed[bucket] = revoke === undefined ? merged : merged.filter((c) => !revoke.includes(c));\n }\n\n // A feature with no surviving platform membership disappears from the effective set.\n // Iterates PLATFORMS rather than naming buckets — the web/mobile-only version silently\n // dropped a grant surviving only on an API bucket.\n if (PLATFORMS.every((bucket) => composed[bucket] === undefined)) continue;\n result[code] = composed;\n }\n\n return result;\n}\n","import { buildSiteCatalog, findFeatureByCode } from './catalog.builder';\nimport { buildDependsMap, filterGrantedByDeps } from './permission-deps';\nimport type {\n FeatureUnlocks,\n LockReason,\n PlatformBucket,\n ScopeType,\n ServiceCode,\n SiteFeatureLocks,\n SiteType,\n VersionSnapshot,\n} from './types';\nimport { isApiBucket, snapshotFeatureKey } from './types';\n\n/**\n * The caller's surface, as the caller reports it.\n *\n * Finer than `PlatformBucket` on the mobile side — `ios` and `android` load different remote\n * entries but share one grant bucket. The API platforms are one-to-one with their buckets: an\n * API client has no variants because it has no UI.\n */\nexport type ClientPlatform = 'web' | 'ios' | 'android' | 'graphql' | 'http';\n\n// Exhaustive by type, so adding a ClientPlatform without deciding its bucket fails the build instead\n// of silently falling through to mobile — which is how an API caller would end up resolving a UI bucket.\nconst BUCKET_BY_CLIENT: Record<ClientPlatform, PlatformBucket> = {\n web: 'web',\n ios: 'mobile',\n android: 'mobile',\n graphql: 'graphql',\n http: 'http',\n};\n\n/**\n * Stands in for the microfrontend an API client does not load.\n *\n * `PermissionFeature.route` is non-optional and read by the web sidebar and the mobile host to\n * mount a remote. Nothing on the API paths reads it — the permission interceptor uses `code`,\n * `permissions` and `locked` — so an empty route keeps one shape for every bucket instead of\n * widening the field to null across every consumer.\n */\nconst EMPTY_ROUTE = { remoteEntry: '', exposedModule: '', routePrefix: '' };\n\nexport interface LockedPermission {\n code: string;\n reason: LockReason | null;\n unlockPlans: string[];\n missingServices: ServiceCode[];\n}\n\nexport interface PlanUpsell {\n plan: string;\n features: string[];\n}\n\nexport interface PermissionFeature {\n code: string;\n name: string;\n lucideIcon: string | null;\n sfSymbol: string;\n materialSymbol: string;\n permissions: string[];\n locked: boolean;\n lockReason: LockReason | null;\n unlockPlans: string[];\n // Which declared services the org has not provisioned — empty unless lockReason is 'SERVICE'\n missingServices: ServiceCode[];\n lockedPermissions: LockedPermission[];\n upsell: PlanUpsell[];\n route: {\n remoteEntry: string;\n exposedModule: string;\n routePrefix: string;\n };\n appCode: string;\n appName: string;\n appIcon: string | null;\n appSortOrder: number;\n}\n\nexport interface ResolveUserFeaturesParams {\n snapshot: VersionSnapshot;\n businessCode: string;\n planCode: string | undefined;\n siteLocks: SiteFeatureLocks | undefined;\n roleFeatures: FeatureUnlocks;\n platform: ClientPlatform;\n siteType?: SiteType;\n scope?: ScopeType;\n // External services the org has provisioned; omitting it locks every service-dependent feature\n availableServices?: ServiceCode[];\n}\n\n// Resolves the features + MF config a user sees at a BU: plan ∧ BU catalog intersected with the role's grants, filtered to the requested platform\nexport function resolveUserFeatures(params: ResolveUserFeaturesParams): PermissionFeature[] {\n const { snapshot, businessCode, planCode, siteLocks, platform, siteType, scope, availableServices } = params;\n\n // Plan unlocks, BU locks, and role grants are stored per platform; resolve only the requesting\n // surface's bucket (web → web; ios/android → mobile; graphql/http → themselves)\n const bucket: PlatformBucket = BUCKET_BY_CLIENT[platform];\n\n const roleFeatures = params.roleFeatures;\n\n // Grants/plans/locks key features by bare code; resolve to the workspace scope's variant (or any variant when unscoped)\n const featureByCode = (code: string) =>\n scope ? snapshot.features[snapshotFeatureKey(code, scope)] : findFeatureByCode(snapshot, code);\n\n // Plan ∧ BU overlay for this bucket, filtered to features that apply to this workspace scope and node type — emits EVERY applicable business feature (plan non-members come out fully locked)\n const catalog = buildSiteCatalog(\n snapshot,\n businessCode,\n planCode,\n siteLocks,\n bucket,\n siteType,\n scope,\n availableServices,\n );\n const catalogMap = new Map(catalog.map((f) => [f.code, f]));\n\n // Per-plan feature-name delta vs the current plan — feeds the plan-locked upsell screen\n const businessPlans = snapshot.businesses[businessCode]?.plans ?? {};\n const currentUnlockedCodes = new Set<string>();\n if (planCode && businessPlans[planCode]) {\n for (const [featureCode, platforms] of Object.entries(businessPlans[planCode].unlockedPermissions)) {\n if (platforms[bucket] !== undefined) currentUnlockedCodes.add(featureCode);\n }\n }\n const planAdds = new Map<string, Array<{ code: string; name: string }>>();\n for (const [planKey, plan] of Object.entries(businessPlans)) {\n if (planKey === planCode) continue;\n const adds: Array<{ code: string; name: string }> = [];\n for (const [featureCode, platforms] of Object.entries(plan.unlockedPermissions)) {\n if (platforms[bucket] === undefined || currentUnlockedCodes.has(featureCode)) continue;\n const name = featureByCode(featureCode)?.name;\n if (name) adds.push({ code: featureCode, name });\n }\n planAdds.set(planKey, adds);\n }\n\n // Granted permission set per feature, taking only this platform's grants\n const grantedFeatures = new Map<string, Set<string>>();\n for (const [code, grant] of Object.entries(roleFeatures)) {\n // Membership is the gate: undefined = not a member on this platform; [] = member with no actions (view-only)\n const granted = grant[bucket];\n if (granted === undefined) continue;\n if (!grantedFeatures.has(code)) grantedFeatures.set(code, new Set());\n for (const perm of granted) grantedFeatures.get(code)?.add(perm);\n }\n\n // Cross-reference the granted features with the catalog to build the response\n const features: PermissionFeature[] = [];\n for (const [code, permsSet] of grantedFeatures) {\n const catalogEntry = catalogMap.get(code);\n if (!catalogEntry) continue;\n\n // A UI bucket reaches its feature by loading a microfrontend, so a feature not published to\n // this platform is omitted rather than handed over as an unloadable tile. An API client loads\n // nothing — requiring a route there would make every headless feature permanently ungrantable.\n const route = isApiBucket(bucket) ? EMPTY_ROUTE : pickRouteForPlatform(catalogEntry, platform);\n if (!route) continue;\n\n // Drop granted permissions whose intra-feature prerequisites aren't also granted (e.g. add needs view)\n const featureDeps = buildDependsMap(featureByCode(code)?.permissions ?? []);\n // Plan/BU lock a subset of permissions; surface which GRANTED ones are locked + why + how to unlock (upsell)\n const permByCode = new Map(catalogEntry.permissions.map((p) => [p.code, p]));\n // Intersected with the catalog, which now omits codes this surface does not implement. Without\n // this a grant made before the flags existed — or written straight through the API — would keep\n // resolving on a bucket where no route enforces it. The picker filtering alone is cosmetic; this\n // is what makes an unimplemented grant genuinely inert.\n const grantedPerms = [...filterGrantedByDeps(permsSet, featureDeps)].filter((c) => permByCode.has(c));\n const lockedPermissions: LockedPermission[] = grantedPerms\n .map((c) => permByCode.get(c))\n .filter((p): p is NonNullable<typeof p> => !!p?.locked)\n .map((p) => ({\n code: p.code,\n reason: p.lockReason ?? null,\n unlockPlans: p.unlockPlans,\n missingServices: p.missingServices,\n }));\n\n // For a plan-locked feature, list the extra features each unlocking plan would add (excluding this feature)\n const upsell: PlanUpsell[] =\n catalogEntry.locked && catalogEntry.lockReason === 'PLAN'\n ? catalogEntry.unlockPlans\n .map((plan) => ({\n plan,\n features: (planAdds.get(plan) ?? []).filter((f) => f.code !== code).map((f) => f.name),\n }))\n .filter((group) => group.features.length > 0)\n : [];\n\n features.push({\n code,\n name: catalogEntry.name,\n lucideIcon: catalogEntry.lucideIcon,\n sfSymbol: catalogEntry.sfSymbol,\n materialSymbol: catalogEntry.materialSymbol,\n permissions: grantedPerms,\n locked: catalogEntry.locked ?? false,\n lockReason: catalogEntry.lockReason ?? null,\n unlockPlans: catalogEntry.unlockPlans,\n missingServices: catalogEntry.missingServices,\n lockedPermissions,\n upsell,\n route,\n appCode: catalogEntry.appCode,\n appName: catalogEntry.appName,\n appIcon: catalogEntry.appIcon,\n appSortOrder: catalogEntry.appSortOrder,\n });\n }\n\n // Order app-alphabetically so the core-web sidebar (groups by app) renders apps sorted without any frontend re-sort;\n // stable sort keeps each app's features in their existing relative order\n features.sort((a, b) => a.appName.localeCompare(b.appName));\n\n return features;\n}\n\n// Selects the route block from a catalog entry for the requested platform, or null when it doesn't publish there\nexport function pickRouteForPlatform(\n entry: {\n web: {\n remoteEntry: string;\n exposedModule: string;\n routePrefix: string;\n } | null;\n mobile: {\n remoteEntryAndroid: string;\n remoteEntryIos: string;\n exposedModule: string;\n routePrefix: string;\n } | null;\n },\n platform: ClientPlatform,\n): { remoteEntry: string; exposedModule: string; routePrefix: string } | null {\n // API platforms load nothing — resolveUserFeatures never routes them here, and answering with the\n // web block for an unhandled value would hand an API caller a remote it cannot mount\n if (platform === 'graphql' || platform === 'http') return null;\n if (platform === 'ios' || platform === 'android') {\n if (!entry.mobile) return null;\n return {\n remoteEntry: platform === 'ios' ? entry.mobile.remoteEntryIos : entry.mobile.remoteEntryAndroid,\n exposedModule: entry.mobile.exposedModule,\n routePrefix: entry.mobile.routePrefix,\n };\n }\n // Web\n if (!entry.web) return null;\n return {\n remoteEntry: entry.web.remoteEntry,\n exposedModule: entry.web.exposedModule,\n routePrefix: entry.web.routePrefix,\n };\n}\n","import { featureAppliesAtNode, isPlanMember, isSiteLockedOnPlatform } from './catalog.builder';\nimport {\n API_BUCKETS,\n type ApiSurface,\n type PlatformBucket,\n type ScopeType,\n type SiteFeatureLocks,\n type SiteType,\n type SnapshotPlan,\n SURFACE_BY_BUCKET,\n snapshotFeatureKey,\n UI_PLATFORMS,\n type VersionSnapshot,\n} from './types';\n\nexport interface SiteMatrixCell {\n inPlan: boolean;\n selected: boolean;\n availableIn: string[];\n}\n\nexport interface SiteMatrixPermission {\n code: string;\n label: string;\n dependsOn: string[];\n web: SiteMatrixCell | null;\n mobile: SiteMatrixCell | null;\n graphql: SiteMatrixCell | null;\n http: SiteMatrixCell | null;\n}\n\nexport interface SiteMatrixFeature {\n code: string;\n name: string;\n icon: string | null;\n scope: ScopeType;\n applicableSiteTypes: SiteType[];\n platforms: PlatformBucket[];\n inPlan: boolean;\n availableIn: string[];\n // The API surfaces the feature declares — what lets the app-credential editor filter by the\n // credential's type.\n apiSurfaces: ApiSurface[];\n permissions: SiteMatrixPermission[];\n}\n\nexport interface MatrixCounts {\n unlocked: number;\n total: number;\n}\n\nexport interface SiteMatrixApp {\n code: string;\n name: string;\n icon: string | null;\n // Counted per surface, not as one total: a consumer showing a subset of the columns (the\n // app-credential editor shows exactly one) sums the surfaces it renders. One number covering all\n // four read as \"20/20 unlocked\" above the 5 checkboxes actually on screen.\n counts: Record<PlatformBucket, MatrixCounts>;\n features: SiteMatrixFeature[];\n}\n\nexport interface SiteMatrix {\n plan: { code: string; name: string };\n apps: SiteMatrixApp[];\n locks: SiteFeatureLocks;\n}\n\n// Builds the SITE-only apps/features/permissions matrix — not filtered to plan members; plan-locked items carry inPlan=false + availableIn\nexport function buildSiteMatrix(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n siteLocks: SiteFeatureLocks | undefined,\n siteType?: SiteType,\n): SiteMatrix {\n return buildMatrix(snapshot, businessCode, planCode, siteLocks, false, siteType);\n}\n\n// Builds the all-scopes apps/features/permissions matrix — every scope's features included, each carrying its real scope; powers the Plan Overview + Create Custom Role picker\nexport function buildPlanMatrix(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n siteLocks?: SiteFeatureLocks,\n): SiteMatrix {\n return buildMatrix(snapshot, businessCode, planCode, siteLocks, true);\n}\n\n// Shared matrix builder — allScopes=false keeps only SITE refs; allScopes=true includes every scope and emits each feature's real scope\nfunction buildMatrix(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n siteLocks: SiteFeatureLocks | undefined,\n allScopes: boolean,\n siteType?: SiteType,\n): SiteMatrix {\n const business = businessCode ? snapshot.businesses[businessCode] : undefined;\n const plans = business?.plans ?? {};\n const plan = planCode ? plans[planCode] : undefined;\n const planMeta = { code: planCode ?? '', name: plan?.name ?? planCode ?? '' };\n const locks = siteLocks ?? {};\n if (!business || !plan) return { plan: planMeta, apps: [], locks };\n\n const apps: SiteMatrixApp[] = [];\n for (const app of snapshot.apps) {\n const counts: Record<PlatformBucket, MatrixCounts> = {\n web: { unlocked: 0, total: 0 },\n mobile: { unlocked: 0, total: 0 },\n graphql: { unlocked: 0, total: 0 },\n http: { unlocked: 0, total: 0 },\n };\n const features: SiteMatrixFeature[] = [];\n\n for (const ref of app.features) {\n if (!allScopes && ref.scope !== 'SITE') continue;\n const code = ref.code;\n const feature = snapshot.features[snapshotFeatureKey(code, ref.scope)];\n if (!feature) continue;\n if (siteType !== undefined && !featureAppliesAtNode(feature.applicableSiteTypes, siteType)) continue;\n // A UI bucket is offered only where the feature publishes a microfrontend; each API bucket is\n // offered where the feature declares its surface. An undeclared surface shows an em dash like\n // a missing microfrontend does.\n const platforms: PlatformBucket[] = [\n ...UI_PLATFORMS.filter((p) => !!feature.microfrontends?.[p]),\n ...API_BUCKETS.filter((b) => feature.apiSurfaces.includes(SURFACE_BY_BUCKET[b])),\n ];\n\n const groupByCode = new Map(feature.permissionGroups.map((g) => [g.code, g]));\n const membership = plan.unlockedPermissions[code];\n const featureInPlan = isPlanMember(membership);\n const siteEntry = siteLocks?.[code];\n\n const permissions: SiteMatrixPermission[] = feature.permissions\n .filter((p) => p.isGlobal || p.businesses.includes(businessCode ?? ''))\n .map((p) => {\n const cell = (plat: PlatformBucket): SiteMatrixCell | null => {\n // The feature must reach this bucket AND this code must be implemented on it — the same\n // two gates buildSiteCatalog applies, so the matrix and the catalog cannot disagree\n if (!platforms.includes(plat) || !p.platforms.includes(plat)) return null;\n const planCodes = membership?.[plat];\n const inPlan = featureInPlan && planCodes !== undefined && planCodes.includes(p.code);\n // Deny-list: an in-plan cell is selected unless the site locks it on this platform\n const selected = inPlan && !isSiteLockedOnPlatform(siteEntry, plat, p.code);\n const availableIn = inPlan ? [] : plansUnlockingPerm(plans, code, p.code, plat, planCode);\n counts[plat].total += 1;\n if (inPlan) counts[plat].unlocked += 1;\n return { inPlan, selected, availableIn };\n };\n return {\n code: p.code,\n label: p.label,\n dependsOn: p.dependsOn,\n group: p.group ? groupByCode.get(p.group) : undefined,\n web: cell('web'),\n mobile: cell('mobile'),\n graphql: cell('graphql'),\n http: cell('http'),\n };\n });\n\n features.push({\n code: feature.code,\n name: feature.name,\n icon: feature.lucideIcon ?? null,\n scope: feature.scope,\n applicableSiteTypes: feature.applicableSiteTypes,\n platforms,\n inPlan: featureInPlan,\n availableIn: featureInPlan ? [] : plansIncludingFeature(plans, code, planCode),\n apiSurfaces: feature.apiSurfaces,\n permissions,\n });\n }\n\n if (features.length === 0) continue;\n apps.push({ code: app.code, name: app.name, icon: app.icon ?? null, counts, features });\n }\n\n // Emit apps alphabetically by name so every consumer (Plan Overview, Role picker, all Locks screens) renders them sorted\n apps.sort((a, b) => a.name.localeCompare(b.name));\n\n return { plan: planMeta, apps, locks };\n}\n\n// Names of other plans (excluding the org's own) that unlock this feature+permission on the given platform\nfunction plansUnlockingPerm(\n plans: Record<string, SnapshotPlan>,\n featureCode: string,\n permCode: string,\n platform: PlatformBucket,\n excludeCode: string | undefined,\n): string[] {\n const names: string[] = [];\n for (const [code, p] of Object.entries(plans)) {\n if (code === excludeCode) continue;\n if ((p.unlockedPermissions[featureCode]?.[platform] ?? []).includes(permCode)) names.push(p.name);\n }\n return names;\n}\n\n// Names of other plans (excluding the org's own) that include this feature at all (membership) — feature-level upsell\nfunction plansIncludingFeature(\n plans: Record<string, SnapshotPlan>,\n featureCode: string,\n excludeCode: string | undefined,\n): string[] {\n const names: string[] = [];\n for (const [code, p] of Object.entries(plans)) {\n if (code === excludeCode) continue;\n if (isPlanMember(p.unlockedPermissions[featureCode])) names.push(p.name);\n }\n return names;\n}\n"],"mappings":";;;;AAKO,SAASA,gBAAgBC,aAA0D;AACxF,QAAMC,UAAU,IAAIC,IAAIF,YAAYG,IAAI,CAACC,MAAMA,EAAEC,IAAI,CAAA;AACrD,QAAMF,MAAkB,oBAAIG,IAAAA;AAC5B,aAAWF,KAAKJ,aAAa;AAC3BG,QAAII,IACFH,EAAEC,OACDD,EAAEI,aAAa,CAAA,GAAIC,OAAO,CAACC,QAAQA,QAAQN,EAAEC,QAAQJ,QAAQU,IAAID,GAAAA,CAAAA,CAAAA;EAEtE;AACA,SAAOP;AACT;AAVgBJ;AAaT,SAASa,cAAcP,MAAcQ,MAAgB;AAC1D,QAAMC,MAAM,oBAAIZ,IAAAA;AAChB,QAAMa,OAAO,oBAAIb,IAAY;IAACG;GAAK;AACnC,QAAMW,QAAQ;IAACX;;AACf,SAAOW,MAAMC,SAAS,GAAG;AACvB,UAAMC,UAAUF,MAAMG,IAAG;AACzB,eAAWT,OAAOG,KAAKO,IAAIF,OAAAA,KAAY,CAAA,GAAI;AACzC,UAAIH,KAAKJ,IAAID,GAAAA,EAAM;AACnBK,WAAKM,IAAIX,GAAAA;AACTI,UAAIO,IAAIX,GAAAA;AACRM,YAAMM,KAAKZ,GAAAA;IACb;EACF;AACA,SAAO;OAAII;;AACb;AAdgBF;AAiBT,SAASW,cAAcC,OAAiBC,gBAA6BZ,MAAgB;AAC1F,QAAMa,SAAS,oBAAIxB,IAAAA;AACnB,QAAMyB,WAAW,oBAAIzB,IAAAA;AACrB,QAAM0B,QAAQ,wBAACvB,SAAAA;AACb,QAAIqB,OAAOf,IAAIN,IAAAA,EAAO,QAAO;AAC7B,QAAIoB,eAAed,IAAIN,IAAAA,GAAO;AAC5BqB,aAAOL,IAAIhB,IAAAA;AACX,aAAO;IACT;AACA,QAAIsB,SAAShB,IAAIN,IAAAA,EAAO,QAAO;AAC/BsB,aAASN,IAAIhB,IAAAA;AACb,UAAMwB,UAAUhB,KAAKO,IAAIf,IAAAA,KAAS,CAAA,GAAIyB,KAAKF,KAAAA;AAC3CD,aAASI,OAAO1B,IAAAA;AAChB,QAAIwB,OAAQH,QAAOL,IAAIhB,IAAAA;AACvB,WAAOwB;EACT,GAZc;AAad,aAAWxB,QAAQmB,MAAOI,OAAMvB,IAAAA;AAChC,SAAOqB;AACT;AAlBgBH;AAqBT,SAASS,oBAAoBC,SAAsBpB,MAAgB;AACxE,QAAMqB,KAAK,oBAAIhC,IAAAA;AACf,QAAMyB,WAAW,oBAAIzB,IAAAA;AACrB,QAAM0B,QAAQ,wBAACvB,SAAAA;AACb,QAAI6B,GAAGvB,IAAIN,IAAAA,EAAO,QAAO;AACzB,QAAI,CAAC4B,QAAQtB,IAAIN,IAAAA,EAAO,QAAO;AAC/B,QAAIsB,SAAShB,IAAIN,IAAAA,EAAO,QAAO;AAC/BsB,aAASN,IAAIhB,IAAAA;AACb,UAAM8B,aAAatB,KAAKO,IAAIf,IAAAA,KAAS,CAAA,GAAI+B,MAAMR,KAAAA;AAC/CD,aAASI,OAAO1B,IAAAA;AAChB,QAAI8B,UAAWD,IAAGb,IAAIhB,IAAAA;AACtB,WAAO8B;EACT,GATc;AAUd,aAAW9B,QAAQ4B,QAASL,OAAMvB,IAAAA;AAClC,SAAO6B;AACT;AAfgBF;;;ACxCT,IAAMK,YAA8B;EAAC;EAAO;EAAU;EAAW;;AAKjE,IAAMC,eAAmC;EAAC;EAAO;;AAIjD,IAAMC,eAAe;EAAC;EAAW;;AAMjC,IAAMC,cAA2B;EAAC;EAAW;;AAE7C,IAAMC,oBAAmD;EAAEC,SAAS;EAAWC,MAAM;AAAO;AAC5F,IAAMC,oBAAmD;EAAEC,SAAS;EAAWC,MAAM;AAAO;AAE5F,SAASC,YAAYC,QAAsB;AAChD,SAAOA,WAAW,aAAaA,WAAW;AAC5C;AAFgBD;AA+DT,IAAME,aAAyB;EAAC;EAAU;EAAa;;AAGvD,IAAMC,gBAAgB;EAAC;;AAqEvB,SAASC,mBAAmBC,MAAcC,OAAgB;AAC/D,SAAO,GAAGA,KAAAA,IAASD,IAAAA;AACrB;AAFgBD;AAIT,IAAMG,0BAA0B;;;AC3JhC,SAASC,qBAAqBC,qBAAiCC,UAAkB;AACtF,SAAOD,oBAAoBE,SAASD,QAAAA;AACtC;AAFgBF;AAKT,SAASI,kBAAkBC,UAA2BC,MAAY;AACvE,aAAWC,WAAWC,OAAOC,OAAOJ,SAASK,QAAQ,GAAG;AACtD,QAAIH,QAAQD,SAASA,KAAM,QAAOC;EACpC;AACA,SAAOI;AACT;AALgBP;AAST,SAASQ,iBACdP,UACAQ,cACAC,UACAC,WACAC,QACAd,UACAe,OACAC,oBAAmC,CAAA,GAAE;AAErC,MAAI,CAACL,aAAc,QAAO,CAAA;AAC1B,QAAMM,WAAWd,SAASe,WAAWP,YAAAA;AACrC,MAAI,CAACM,SAAU,QAAO,CAAA;AACtB,QAAME,QAAQF,SAASE;AACvB,QAAMC,OAAOR,WAAWO,MAAMP,QAAAA,IAAYH;AAC1C,QAAMY,QAAQR;AAEd,QAAMS,UAAiC,CAAA;AAEvC,QAAMC,aAAa;OAAIpB,SAASqB;IAAMC,KAAK,CAACC,GAAGC,MAAMD,EAAEE,KAAKC,cAAcF,EAAEC,IAAI,CAAA;AAChF,aAAWE,OAAOP,YAAY;AAE5B,UAAMQ,sBAAsBD,IAAItB,SAC7BwB,OAAO,CAACC,QAAQlB,UAAUN,UAAawB,IAAIlB,UAAUA,KAAAA,EACrDmB,IAAI,CAACD,QAAQ9B,SAASK,SAAS2B,mBAAmBF,IAAI7B,MAAM6B,IAAIlB,KAAK,CAAA,CAAE,EACvEiB,OACC,CAACI,MACC,CAAC,CAACA;;;;KAKDC,YAAYvB,MAAAA,IACTwB,cAAcF,EAAEG,aAAaC,kBAAkB1B,MAAAA,CAAO,IACtD,CAAC,EAAEsB,EAAEK,gBAAgBC,OAAON,EAAEK,gBAAgBE,aACjD3C,aAAaS,UAAaX,qBAAqBsC,EAAErC,qBAAqBC,QAAAA,EAAQ;AAGrF,QAAI+B,oBAAoBa,WAAW,EAAG;AAGtC,eAAWvC,WAAW0B,qBAAqB;AACzC,YAAMc,aAAazB,MAAM0B,oBAAoBzC,QAAQD,IAAI;AAEzD,YAAMsC,MAAMrC,QAAQoC,gBAAgBC;AACpC,YAAMC,SAAStC,QAAQoC,gBAAgBE;AAIvC,YAAMI,iBAAiBF,aAAa/B,MAAAA,MAAYL;AAChD,YAAMuC,qBAAqB3B,QAAQhB,QAAQD,IAAI,IAAIU,MAAAA,MAAY;AAC/D,YAAMmC,kBAAkBC,cAAc7C,SAASW,iBAAAA;AAG/C,YAAMmC,cAAcC,iBAAiB/C,SAASM,cAAckC,YAAYxB,OAAOF,OAAOL,QAAQmC,eAAAA;AAC9F,YAAMI,aAAaC,kBAAkB,CAACP,gBAAgBC,oBAAoBC,eAAAA;AAC1E,YAAMM,SAASF,eAAe;AAC9B,YAAMG,cAAcH,eAAe,SAASI,sBAAsBtC,OAAOd,QAAQD,MAAMU,MAAAA,IAAU,CAAA;AAEjGQ,cAAQoC,KAAK;QACXtD,MAAMC,QAAQD;QACdwB,MAAMvB,QAAQuB;QACd+B,YAAYtD,QAAQsD,cAAc;QAClCC,UAAUvD,QAAQuD,YAAY;QAC9BC,gBAAgBxD,QAAQwD,kBAAkB;QAC1CnB,KAAKA,MACD;UACEoB,aAAapB,IAAIoB,eAAe;UAChCC,eAAerB,IAAIqB,iBAAiB;UACpCC,aAAatB,IAAIsB,eAAe;QAClC,IACA;QACJrB,QAAQA,SACJ;UACEsB,oBAAoBtB,OAAOsB,sBAAsB;UACjDC,gBAAgBvB,OAAOuB,kBAAkB;UACzCH,eAAepB,OAAOoB,iBAAiB;UACvCC,aAAarB,OAAOqB,eAAe;QACrC,IACA;QACJG,SAASrC,IAAI1B;QACbgE,SAAStC,IAAIF;QACbyC,SAASvC,IAAIwC,QAAQ;QACrBC,cAAczC,IAAI0C,aAAa;QAC/BjB;QACAF;QACAG;QACAP;QACAE;MACF,CAAA;IACF;EACF;AACA,SAAO7B;AACT;AA7FgBZ;AAgGT,SAAS+D,aAAaC,OAAgC;AAC3D,MAAI,CAACA,MAAO,QAAO;AACnB,SAAOC,UAAUC,KAAK,CAACC,aAAaH,MAAMG,QAAAA,MAAcpE,MAAAA;AAC1D;AAHgBgE;AAWT,SAASnC,cAAcwC,UAAwBC,SAA+B;AACnF,SAAOA,YAAYtE,UAAaqE,SAAS7E,SAAS8E,OAAAA;AACpD;AAFgBzC;AAOhB,SAASgB,kBACP0B,YACAC,YACAhC,iBAA8B;AAE9B,MAAI+B,WAAY,QAAO;AACvB,MAAIC,WAAY,QAAO;AACvB,MAAIhC,gBAAgBL,SAAS,EAAG,QAAO;AACvC,SAAO;AACT;AATSU;AAYT,SAASJ,cAAc7C,SAA0BW,mBAAgC;AAC/E,SAAOX,QAAQ6E,iBAAiBlD,OAAO,CAACmD,YAAY,CAACnE,kBAAkBf,SAASkF,OAAAA,CAAAA;AAClF;AAFSjC;AAKF,SAASkC,uBACdV,OACAG,UACAzE,MAAY;AAEZ,QAAMiB,QAAQqD,QAAQG,QAAAA;AACtB,SAAOxD,UAAU,SAASA,OAAOpB,SAASG,IAAAA,KAAS;AACrD;AAPgBgF;AAWhB,SAAShC,iBACP/C,SACAM,cACA0E,gBACAxE,WACAM,OACAL,QACAmC,kBAAiC,CAAA,GAAE;AAEnC,QAAMqC,eAAe,IAAIC,IAAIF,iBAAiBvE,MAAAA,KAAW,CAAA,CAAE;AAC3D,QAAM0E,YAAY3E,YAAYR,QAAQD,IAAI;AAK1C,QAAMqF,QAAQpF,QAAQ8C,YACnBnB,OAAO,CAAC0D,MAAMA,EAAEC,YAAYD,EAAExE,WAAWjB,SAASU,YAAAA,CAAAA,EAClDqB,OAAO,CAAC0D,MAAMA,EAAEE,UAAU3F,SAASa,MAAAA,CAAAA;AACtC,QAAM+E,OAAOC,gBAAgBL,KAAAA;AAC7B,QAAMM,QAAQN,MAAMvD,IAAI,CAACwD,MAAMA,EAAEtF,IAAI;AAGrC,QAAM4F,qBAAqB,oBAAIT,IAAAA;AAC/B,QAAMU,qBAAqB,oBAAIV,IAAAA;AAC/B,aAAWG,KAAKD,OAAO;AACrB,QAAI,CAACH,aAAaY,IAAIR,EAAEtF,IAAI,EAAG4F,oBAAmBG,IAAIT,EAAEtF,IAAI;AAC5D,QAAIgF,uBAAuBI,WAAW1E,QAAQ4E,EAAEtF,IAAI,EAAG6F,oBAAmBE,IAAIT,EAAEtF,IAAI;EACtF;AACA,QAAMgG,iBAAiB,oBAAIb,IAAY;OAAIS;OAAuBC;GAAmB;AACrF,QAAMI,YAAYC,cAAcP,OAAOK,gBAAgBP,IAAAA;AAEvD,SAAOJ,MAAMvD,IAAI,CAACwD,MAAAA;AAEhB,UAAMa,UAAU;MAACb,EAAEtF;SAASoG,cAAcd,EAAEtF,MAAMyF,IAAAA;;AAClD,UAAMY,WAAWJ,UAAUH,IAAIR,EAAEtF,IAAI;AACrC,UAAMsG,aAAaD,YAAYF,QAAQ3B,KAAK,CAAC+B,MAAMX,mBAAmBE,IAAIS,CAAAA,CAAAA;AAC1E,UAAMC,aAAaH,YAAYF,QAAQ3B,KAAK,CAAC+B,MAAMV,mBAAmBC,IAAIS,CAAAA,CAAAA;AAC1E,UAAMtD,aAAaC,kBAAkBoD,YAAYE,YAAY3D,eAAAA;AAC7D,UAAMM,SAASF,eAAe;AAC9B,UAAMG,cAAcH,eAAe,SAASwD,sBAAsB1F,OAAOd,QAAQD,MAAMmG,SAASzF,MAAAA,IAAU,CAAA;AAC1G,WAAO;MAAEV,MAAMsF,EAAEtF;MAAMmD;MAAQF;MAAYG;MAAaP;IAAgB;EAC1E,CAAA;AACF;AA1CSG;AA6CT,SAASyD,sBACP1F,OACA2F,aACAP,SACAzF,QAAsB;AAEtB,QAAMiG,SAAmB,CAAA;AACzB,aAAW,CAAC3G,MAAMgB,IAAAA,KAASd,OAAO0G,QAAQ7F,KAAAA,GAAQ;AAChD,UAAM8F,WAAW7F,KAAK0B,oBAAoBgE,WAAAA,IAAehG,MAAAA;AACzD,QAAImG,YAAYV,QAAQW,MAAM,CAACP,MAAMM,SAAShH,SAAS0G,CAAAA,CAAAA,EAAKI,QAAOrD,KAAKtD,IAAAA;EAC1E;AACA,SAAO2G;AACT;AAZSF;AAeT,SAASpD,sBACPtC,OACA2F,aACAhG,QAAsB;AAEtB,QAAMiG,SAAmB,CAAA;AACzB,aAAW,CAAC3G,MAAMgB,IAAAA,KAASd,OAAO0G,QAAQ7F,KAAAA,GAAQ;AAChD,QAAIC,KAAK0B,oBAAoBgE,WAAAA,IAAehG,MAAAA,MAAYL,OAAWsG,QAAOrD,KAAKtD,IAAAA;EACjF;AACA,SAAO2G;AACT;AAVStD;AAaF,SAAS0D,eAAehH,UAA2BQ,cAAgC;AACxF,MAAI,CAACA,aAAc,QAAO,CAAA;AAC1B,QAAMM,WAAWd,SAASe,WAAWP,YAAAA;AACrC,MAAI,CAACM,SAAU,QAAO,CAAA;AACtB,SAAOX,OAAOC,OAAOU,SAASmG,aAAa;AAC7C;AALgBD;;;AC9OhB,SAASE,YAAYC,MAA4BC,KAAyB;AACxE,MAAID,SAASE,UAAaD,QAAQC,OAAW,QAAOA;AACpD,SAAO;OAAI,oBAAIC,IAAI;SAAKH,QAAQ,CAAA;SAASC,OAAO,CAAA;KAAI;;AACtD;AAHSF;AAMF,SAASK,kBAAkBC,QAA+B;AAC/D,QAAM,EAAEC,cAAcC,WAAWC,QAAO,IAAKH;AAE7C,QAAMI,SAAyB,CAAC;AAChC,QAAMC,eAAe,oBAAIP,IAAI;OAAIQ,OAAOC,KAAKN,gBAAgB,CAAC,CAAA;OAAOK,OAAOC,KAAKL,SAAAA;GAAW;AAE5F,aAAWM,QAAQH,cAAc;AAC/B,UAAMV,OAAOM,eAAeO,IAAAA,KAAS,CAAC;AACtC,UAAMZ,MAAMM,UAAUM,IAAAA,KAAS,CAAC;AAChC,UAAMC,UAAUN,UAAUK,IAAAA;AAE1B,UAAME,WAA0B,CAAC;AACjC,eAAWC,UAAUC,WAAW;AAC9B,YAAMC,SAASnB,YAAYC,KAAKgB,MAAAA,GAASf,IAAIe,MAAAA,CAAO;AACpD,UAAIE,WAAWhB,OAAW;AAC1B,YAAMiB,SAASL,UAAUE,MAAAA;AAEzB,UAAIG,WAAW,KAAM;AACrBJ,eAASC,MAAAA,IAAUG,WAAWjB,SAAYgB,SAASA,OAAOE,OAAO,CAACC,MAAM,CAACF,OAAOG,SAASD,CAAAA,CAAAA;IAC3F;AAKA,QAAIJ,UAAUM,MAAM,CAACP,WAAWD,SAASC,MAAAA,MAAYd,MAAAA,EAAY;AACjEO,WAAOI,IAAAA,IAAQE;EACjB;AAEA,SAAON;AACT;AA7BgBL;;;ACQhB,IAAMoB,mBAA2D;EAC/DC,KAAK;EACLC,KAAK;EACLC,SAAS;EACTC,SAAS;EACTC,MAAM;AACR;AAUA,IAAMC,cAAc;EAAEC,aAAa;EAAIC,eAAe;EAAIC,aAAa;AAAG;AAqDnE,SAASC,oBAAoBC,QAAiC;AACnE,QAAM,EAAEC,UAAUC,cAAcC,UAAUC,WAAWC,UAAUC,UAAUC,OAAOC,kBAAiB,IAAKR;AAItG,QAAMS,SAAyBpB,iBAAiBgB,QAAAA;AAEhD,QAAMK,eAAeV,OAAOU;AAG5B,QAAMC,gBAAgB,wBAACC,SACrBL,QAAQN,SAASY,SAASC,mBAAmBF,MAAML,KAAAA,CAAAA,IAAUQ,kBAAkBd,UAAUW,IAAAA,GADrE;AAItB,QAAMI,UAAUC,iBACdhB,UACAC,cACAC,UACAC,WACAK,QACAH,UACAC,OACAC,iBAAAA;AAEF,QAAMU,aAAa,IAAIC,IAAIH,QAAQI,IAAI,CAACC,MAAM;IAACA,EAAET;IAAMS;GAAE,CAAA;AAGzD,QAAMC,gBAAgBrB,SAASsB,WAAWrB,YAAAA,GAAesB,SAAS,CAAC;AACnE,QAAMC,uBAAuB,oBAAIC,IAAAA;AACjC,MAAIvB,YAAYmB,cAAcnB,QAAAA,GAAW;AACvC,eAAW,CAACwB,aAAaC,SAAAA,KAAcC,OAAOC,QAAQR,cAAcnB,QAAAA,EAAU4B,mBAAmB,GAAG;AAClG,UAAIH,UAAUnB,MAAAA,MAAYuB,OAAWP,sBAAqBQ,IAAIN,WAAAA;IAChE;EACF;AACA,QAAMO,WAAW,oBAAIf,IAAAA;AACrB,aAAW,CAACgB,SAASC,IAAAA,KAASP,OAAOC,QAAQR,aAAAA,GAAgB;AAC3D,QAAIa,YAAYhC,SAAU;AAC1B,UAAMkC,OAA8C,CAAA;AACpD,eAAW,CAACV,aAAaC,SAAAA,KAAcC,OAAOC,QAAQM,KAAKL,mBAAmB,GAAG;AAC/E,UAAIH,UAAUnB,MAAAA,MAAYuB,UAAaP,qBAAqBa,IAAIX,WAAAA,EAAc;AAC9E,YAAMY,OAAO5B,cAAcgB,WAAAA,GAAcY;AACzC,UAAIA,KAAMF,MAAKG,KAAK;QAAE5B,MAAMe;QAAaY;MAAK,CAAA;IAChD;AACAL,aAASO,IAAIN,SAASE,IAAAA;EACxB;AAGA,QAAMK,kBAAkB,oBAAIvB,IAAAA;AAC5B,aAAW,CAACP,MAAM+B,KAAAA,KAAUd,OAAOC,QAAQpB,YAAAA,GAAe;AAExD,UAAMkC,UAAUD,MAAMlC,MAAAA;AACtB,QAAImC,YAAYZ,OAAW;AAC3B,QAAI,CAACU,gBAAgBJ,IAAI1B,IAAAA,EAAO8B,iBAAgBD,IAAI7B,MAAM,oBAAIc,IAAAA,CAAAA;AAC9D,eAAWmB,QAAQD,QAASF,iBAAgBI,IAAIlC,IAAAA,GAAOqB,IAAIY,IAAAA;EAC7D;AAGA,QAAMhC,WAAgC,CAAA;AACtC,aAAW,CAACD,MAAMmC,QAAAA,KAAaL,iBAAiB;AAC9C,UAAMM,eAAe9B,WAAW4B,IAAIlC,IAAAA;AACpC,QAAI,CAACoC,aAAc;AAKnB,UAAMC,QAAQC,YAAYzC,MAAAA,IAAUd,cAAcwD,qBAAqBH,cAAc3C,QAAAA;AACrF,QAAI,CAAC4C,MAAO;AAGZ,UAAMG,cAAcC,gBAAgB1C,cAAcC,IAAAA,GAAO0C,eAAe,CAAA,CAAE;AAE1E,UAAMC,aAAa,IAAIpC,IAAI6B,aAAaM,YAAYlC,IAAI,CAACoC,MAAM;MAACA,EAAE5C;MAAM4C;KAAE,CAAA;AAK1E,UAAMC,eAAe;SAAIC,oBAAoBX,UAAUK,WAAAA;MAAcO,OAAO,CAACC,MAAML,WAAWjB,IAAIsB,CAAAA,CAAAA;AAClG,UAAMC,oBAAwCJ,aAC3CrC,IAAI,CAACwC,MAAML,WAAWT,IAAIc,CAAAA,CAAAA,EAC1BD,OAAO,CAACH,MAAkC,CAAC,CAACA,GAAGM,MAAAA,EAC/C1C,IAAI,CAACoC,OAAO;MACX5C,MAAM4C,EAAE5C;MACRmD,QAAQP,EAAEQ,cAAc;MACxBC,aAAaT,EAAES;MACfC,iBAAiBV,EAAEU;IACrB,EAAA;AAGF,UAAMC,SACJnB,aAAac,UAAUd,aAAagB,eAAe,SAC/ChB,aAAaiB,YACV7C,IAAI,CAACgB,UAAU;MACdA;MACAvB,WAAWqB,SAASY,IAAIV,IAAAA,KAAS,CAAA,GAAIuB,OAAO,CAACtC,MAAMA,EAAET,SAASA,IAAAA,EAAMQ,IAAI,CAACC,MAAMA,EAAEkB,IAAI;IACvF,EAAA,EACCoB,OAAO,CAACS,UAAUA,MAAMvD,SAASwD,SAAS,CAAA,IAC7C,CAAA;AAENxD,aAAS2B,KAAK;MACZ5B;MACA2B,MAAMS,aAAaT;MACnB+B,YAAYtB,aAAasB;MACzBC,UAAUvB,aAAauB;MACvBC,gBAAgBxB,aAAawB;MAC7BlB,aAAaG;MACbK,QAAQd,aAAac,UAAU;MAC/BE,YAAYhB,aAAagB,cAAc;MACvCC,aAAajB,aAAaiB;MAC1BC,iBAAiBlB,aAAakB;MAC9BL;MACAM;MACAlB;MACAwB,SAASzB,aAAayB;MACtBC,SAAS1B,aAAa0B;MACtBC,SAAS3B,aAAa2B;MACtBC,cAAc5B,aAAa4B;IAC7B,CAAA;EACF;AAIA/D,WAASgE,KAAK,CAACC,GAAGC,MAAMD,EAAEJ,QAAQM,cAAcD,EAAEL,OAAO,CAAA;AAEzD,SAAO7D;AACT;AA5HgBd;AA+HT,SAASoD,qBACd8B,OAaA5E,UAAwB;AAIxB,MAAIA,aAAa,aAAaA,aAAa,OAAQ,QAAO;AAC1D,MAAIA,aAAa,SAASA,aAAa,WAAW;AAChD,QAAI,CAAC4E,MAAMC,OAAQ,QAAO;AAC1B,WAAO;MACLtF,aAAaS,aAAa,QAAQ4E,MAAMC,OAAOC,iBAAiBF,MAAMC,OAAOE;MAC7EvF,eAAeoF,MAAMC,OAAOrF;MAC5BC,aAAamF,MAAMC,OAAOpF;IAC5B;EACF;AAEA,MAAI,CAACmF,MAAM3F,IAAK,QAAO;AACvB,SAAO;IACLM,aAAaqF,MAAM3F,IAAIM;IACvBC,eAAeoF,MAAM3F,IAAIO;IACzBC,aAAamF,MAAM3F,IAAIQ;EACzB;AACF;AAlCgBqD;;;ACxJT,SAASkC,gBACdC,UACAC,cACAC,UACAC,WACAC,UAAmB;AAEnB,SAAOC,YAAYL,UAAUC,cAAcC,UAAUC,WAAW,OAAOC,QAAAA;AACzE;AARgBL;AAWT,SAASO,gBACdN,UACAC,cACAC,UACAC,WAA4B;AAE5B,SAAOE,YAAYL,UAAUC,cAAcC,UAAUC,WAAW,IAAA;AAClE;AAPgBG;AAUhB,SAASD,YACPL,UACAC,cACAC,UACAC,WACAI,WACAH,UAAmB;AAEnB,QAAMI,WAAWP,eAAeD,SAASS,WAAWR,YAAAA,IAAgBS;AACpE,QAAMC,QAAQH,UAAUG,SAAS,CAAC;AAClC,QAAMC,OAAOV,WAAWS,MAAMT,QAAAA,IAAYQ;AAC1C,QAAMG,WAAW;IAAEC,MAAMZ,YAAY;IAAIa,MAAMH,MAAMG,QAAQb,YAAY;EAAG;AAC5E,QAAMc,QAAQb,aAAa,CAAC;AAC5B,MAAI,CAACK,YAAY,CAACI,KAAM,QAAO;IAAEA,MAAMC;IAAUI,MAAM,CAAA;IAAID;EAAM;AAEjE,QAAMC,OAAwB,CAAA;AAC9B,aAAWC,OAAOlB,SAASiB,MAAM;AAC/B,UAAME,SAA+C;MACnDC,KAAK;QAAEC,UAAU;QAAGC,OAAO;MAAE;MAC7BC,QAAQ;QAAEF,UAAU;QAAGC,OAAO;MAAE;MAChCE,SAAS;QAAEH,UAAU;QAAGC,OAAO;MAAE;MACjCG,MAAM;QAAEJ,UAAU;QAAGC,OAAO;MAAE;IAChC;AACA,UAAMI,WAAgC,CAAA;AAEtC,eAAWC,OAAOT,IAAIQ,UAAU;AAC9B,UAAI,CAACnB,aAAaoB,IAAIC,UAAU,OAAQ;AACxC,YAAMd,OAAOa,IAAIb;AACjB,YAAMe,UAAU7B,SAAS0B,SAASI,mBAAmBhB,MAAMa,IAAIC,KAAK,CAAA;AACpE,UAAI,CAACC,QAAS;AACd,UAAIzB,aAAaM,UAAa,CAACqB,qBAAqBF,QAAQG,qBAAqB5B,QAAAA,EAAW;AAI5F,YAAM6B,YAA8B;WAC/BC,aAAaC,OAAO,CAACC,MAAM,CAAC,CAACP,QAAQQ,iBAAiBD,CAAAA,CAAE;WACxDE,YAAYH,OAAO,CAACI,MAAMV,QAAQW,YAAYC,SAASC,kBAAkBH,CAAAA,CAAE,CAAA;;AAGhF,YAAMI,cAAc,IAAIC,IAAIf,QAAQgB,iBAAiBC,IAAI,CAACC,MAAM;QAACA,EAAEjC;QAAMiC;OAAE,CAAA;AAC3E,YAAMC,aAAapC,KAAKqC,oBAAoBnC,IAAAA;AAC5C,YAAMoC,gBAAgBC,aAAaH,UAAAA;AACnC,YAAMI,YAAYjD,YAAYW,IAAAA;AAE9B,YAAMuC,cAAsCxB,QAAQwB,YACjDlB,OAAO,CAACC,MAAMA,EAAEkB,YAAYlB,EAAE3B,WAAWgC,SAASxC,gBAAgB,EAAA,CAAA,EAClE6C,IAAI,CAACV,MAAAA;AACJ,cAAMmB,OAAO,wBAACC,SAAAA;AAGZ,cAAI,CAACvB,UAAUQ,SAASe,IAAAA,KAAS,CAACpB,EAAEH,UAAUQ,SAASe,IAAAA,EAAO,QAAO;AACrE,gBAAMC,YAAYT,aAAaQ,IAAAA;AAC/B,gBAAME,SAASR,iBAAiBO,cAAc/C,UAAa+C,UAAUhB,SAASL,EAAEtB,IAAI;AAEpF,gBAAM6C,WAAWD,UAAU,CAACE,uBAAuBR,WAAWI,MAAMpB,EAAEtB,IAAI;AAC1E,gBAAM+C,cAAcH,SAAS,CAAA,IAAKI,mBAAmBnD,OAAOG,MAAMsB,EAAEtB,MAAM0C,MAAMtD,QAAAA;AAChFiB,iBAAOqC,IAAAA,EAAMlC,SAAS;AACtB,cAAIoC,OAAQvC,QAAOqC,IAAAA,EAAMnC,YAAY;AACrC,iBAAO;YAAEqC;YAAQC;YAAUE;UAAY;QACzC,GAZa;AAab,eAAO;UACL/C,MAAMsB,EAAEtB;UACRiD,OAAO3B,EAAE2B;UACTC,WAAW5B,EAAE4B;UACbC,OAAO7B,EAAE6B,QAAQtB,YAAYuB,IAAI9B,EAAE6B,KAAK,IAAIvD;UAC5CU,KAAKmC,KAAK,KAAA;UACVhC,QAAQgC,KAAK,QAAA;UACb/B,SAAS+B,KAAK,SAAA;UACd9B,MAAM8B,KAAK,MAAA;QACb;MACF,CAAA;AAEF7B,eAASyC,KAAK;QACZrD,MAAMe,QAAQf;QACdC,MAAMc,QAAQd;QACdqD,MAAMvC,QAAQwC,cAAc;QAC5BzC,OAAOC,QAAQD;QACfI,qBAAqBH,QAAQG;QAC7BC;QACAyB,QAAQR;QACRW,aAAaX,gBAAgB,CAAA,IAAKoB,uBAAsB3D,OAAOG,MAAMZ,QAAAA;QACrEsC,aAAaX,QAAQW;QACrBa;MACF,CAAA;IACF;AAEA,QAAI3B,SAAS6C,WAAW,EAAG;AAC3BtD,SAAKkD,KAAK;MAAErD,MAAMI,IAAIJ;MAAMC,MAAMG,IAAIH;MAAMqD,MAAMlD,IAAIkD,QAAQ;MAAMjD;MAAQO;IAAS,CAAA;EACvF;AAGAT,OAAKuD,KAAK,CAACC,GAAGlC,MAAMkC,EAAE1D,KAAK2D,cAAcnC,EAAExB,IAAI,CAAA;AAE/C,SAAO;IAAEH,MAAMC;IAAUI;IAAMD;EAAM;AACvC;AA9FSX;AAiGT,SAASyD,mBACPnD,OACAgE,aACAC,UACAC,UACAC,aAA+B;AAE/B,QAAMC,QAAkB,CAAA;AACxB,aAAW,CAACjE,MAAMsB,CAAAA,KAAM4C,OAAOC,QAAQtE,KAAAA,GAAQ;AAC7C,QAAIG,SAASgE,YAAa;AAC1B,SAAK1C,EAAEa,oBAAoB0B,WAAAA,IAAeE,QAAAA,KAAa,CAAA,GAAIpC,SAASmC,QAAAA,EAAWG,OAAMZ,KAAK/B,EAAErB,IAAI;EAClG;AACA,SAAOgE;AACT;AAbSjB;AAgBT,SAASQ,uBACP3D,OACAgE,aACAG,aAA+B;AAE/B,QAAMC,QAAkB,CAAA;AACxB,aAAW,CAACjE,MAAMsB,CAAAA,KAAM4C,OAAOC,QAAQtE,KAAAA,GAAQ;AAC7C,QAAIG,SAASgE,YAAa;AAC1B,QAAI3B,aAAaf,EAAEa,oBAAoB0B,WAAAA,CAAY,EAAGI,OAAMZ,KAAK/B,EAAErB,IAAI;EACzE;AACA,SAAOgE;AACT;AAXST,OAAAA,wBAAAA;","names":["buildDependsMap","permissions","present","Set","map","p","code","Map","set","dependsOn","filter","dep","has","prereqClosure","deps","out","seen","stack","length","current","pop","get","add","push","cascadeLocked","codes","directlyLocked","locked","visiting","check","viaDep","some","delete","filterGrantedByDeps","granted","ok","satisfied","every","PLATFORMS","UI_PLATFORMS","API_SURFACES","API_BUCKETS","SURFACE_BY_BUCKET","graphql","http","BUCKET_BY_SURFACE","GRAPHQL","HTTP","isApiBucket","bucket","SITE_TYPES","SERVICE_CODES","snapshotFeatureKey","code","scope","SNAPSHOT_SCHEMA_VERSION","featureAppliesAtNode","applicableSiteTypes","siteType","includes","findFeatureByCode","snapshot","code","feature","Object","values","features","undefined","buildSiteCatalog","businessCode","planCode","siteLocks","bucket","scope","availableServices","business","businesses","plans","plan","locks","catalog","sortedApps","apps","sort","a","b","name","localeCompare","app","businessAppFeatures","filter","ref","map","snapshotFeatureKey","f","isApiBucket","surfaceAllows","apiSurfaces","SURFACE_BY_BUCKET","microfrontends","web","mobile","length","membership","unlockedPermissions","memberOnBucket","sitePlatformLocked","missingServices","unmetServices","permissions","buildPermissions","lockReason","resolveLockReason","locked","unlockPlans","plansIncludingFeature","push","lucideIcon","sfSymbol","materialSymbol","remoteEntry","exposedModule","routePrefix","remoteEntryAndroid","remoteEntryIos","appCode","appName","appIcon","icon","appSortOrder","sortOrder","isPlanMember","entry","PLATFORMS","some","platform","surfaces","surface","planLocked","siteLocked","requiredServices","service","isSiteLockedOnPlatform","planMembership","planUnlocked","Set","lockEntry","perms","p","isGlobal","platforms","deps","buildDependsMap","codes","directlyPlanLocked","directlySiteLocked","has","add","directlyLocked","lockedSet","cascadeLocked","closure","prereqClosure","cascaded","planReason","c","siteReason","plansUnlockingClosure","featureCode","result","entries","unlocked","every","buildSiteRoles","roleTemplates","unionBucket","base","add","undefined","Set","composeRoleGrants","params","baseFeatures","additions","revoked","result","featureCodes","Object","keys","code","revokes","composed","bucket","PLATFORMS","merged","revoke","filter","c","includes","every","BUCKET_BY_CLIENT","web","ios","android","graphql","http","EMPTY_ROUTE","remoteEntry","exposedModule","routePrefix","resolveUserFeatures","params","snapshot","businessCode","planCode","siteLocks","platform","siteType","scope","availableServices","bucket","roleFeatures","featureByCode","code","features","snapshotFeatureKey","findFeatureByCode","catalog","buildSiteCatalog","catalogMap","Map","map","f","businessPlans","businesses","plans","currentUnlockedCodes","Set","featureCode","platforms","Object","entries","unlockedPermissions","undefined","add","planAdds","planKey","plan","adds","has","name","push","set","grantedFeatures","grant","granted","perm","get","permsSet","catalogEntry","route","isApiBucket","pickRouteForPlatform","featureDeps","buildDependsMap","permissions","permByCode","p","grantedPerms","filterGrantedByDeps","filter","c","lockedPermissions","locked","reason","lockReason","unlockPlans","missingServices","upsell","group","length","lucideIcon","sfSymbol","materialSymbol","appCode","appName","appIcon","appSortOrder","sort","a","b","localeCompare","entry","mobile","remoteEntryIos","remoteEntryAndroid","buildSiteMatrix","snapshot","businessCode","planCode","siteLocks","siteType","buildMatrix","buildPlanMatrix","allScopes","business","businesses","undefined","plans","plan","planMeta","code","name","locks","apps","app","counts","web","unlocked","total","mobile","graphql","http","features","ref","scope","feature","snapshotFeatureKey","featureAppliesAtNode","applicableSiteTypes","platforms","UI_PLATFORMS","filter","p","microfrontends","API_BUCKETS","b","apiSurfaces","includes","SURFACE_BY_BUCKET","groupByCode","Map","permissionGroups","map","g","membership","unlockedPermissions","featureInPlan","isPlanMember","siteEntry","permissions","isGlobal","cell","plat","planCodes","inPlan","selected","isSiteLockedOnPlatform","availableIn","plansUnlockingPerm","label","dependsOn","group","get","push","icon","lucideIcon","plansIncludingFeature","length","sort","a","localeCompare","featureCode","permCode","platform","excludeCode","names","Object","entries"]}
|
package/dist/license.d.cts
CHANGED
package/dist/license.d.ts
CHANGED
|
@@ -129,17 +129,17 @@ interface BusinessVocabulary {
|
|
|
129
129
|
interface SnapshotBusiness {
|
|
130
130
|
name: string;
|
|
131
131
|
vocabulary?: BusinessVocabulary;
|
|
132
|
-
apps: SnapshotApp[];
|
|
133
132
|
roleTemplates: Record<string, SnapshotRoleTemplate>;
|
|
134
133
|
plans: Record<string, SnapshotPlan>;
|
|
135
134
|
}
|
|
136
135
|
interface VersionSnapshot {
|
|
137
136
|
schemaVersion?: number;
|
|
138
137
|
features: Record<string, SnapshotFeature>;
|
|
138
|
+
apps: SnapshotApp[];
|
|
139
139
|
businesses: Record<string, SnapshotBusiness>;
|
|
140
140
|
}
|
|
141
141
|
declare function snapshotFeatureKey(code: string, scope: ScopeType): string;
|
|
142
|
-
declare const SNAPSHOT_SCHEMA_VERSION =
|
|
142
|
+
declare const SNAPSHOT_SCHEMA_VERSION = 6;
|
|
143
143
|
type LockReason = 'PLAN' | 'SITE' | 'SERVICE';
|
|
144
144
|
interface CatalogPermission {
|
|
145
145
|
code: string;
|
|
@@ -129,17 +129,17 @@ interface BusinessVocabulary {
|
|
|
129
129
|
interface SnapshotBusiness {
|
|
130
130
|
name: string;
|
|
131
131
|
vocabulary?: BusinessVocabulary;
|
|
132
|
-
apps: SnapshotApp[];
|
|
133
132
|
roleTemplates: Record<string, SnapshotRoleTemplate>;
|
|
134
133
|
plans: Record<string, SnapshotPlan>;
|
|
135
134
|
}
|
|
136
135
|
interface VersionSnapshot {
|
|
137
136
|
schemaVersion?: number;
|
|
138
137
|
features: Record<string, SnapshotFeature>;
|
|
138
|
+
apps: SnapshotApp[];
|
|
139
139
|
businesses: Record<string, SnapshotBusiness>;
|
|
140
140
|
}
|
|
141
141
|
declare function snapshotFeatureKey(code: string, scope: ScopeType): string;
|
|
142
|
-
declare const SNAPSHOT_SCHEMA_VERSION =
|
|
142
|
+
declare const SNAPSHOT_SCHEMA_VERSION = 6;
|
|
143
143
|
type LockReason = 'PLAN' | 'SITE' | 'SERVICE';
|
|
144
144
|
interface CatalogPermission {
|
|
145
145
|
code: string;
|