@vritti/api-sdk 0.3.3 → 0.3.5

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.
Files changed (133) hide show
  1. package/dist/auth.cjs +942 -0
  2. package/dist/auth.cjs.map +1 -0
  3. package/dist/auth.d.cts +95 -0
  4. package/dist/auth.d.ts +95 -0
  5. package/dist/auth.js +884 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/cache.cjs +182 -14
  8. package/dist/cache.cjs.map +1 -1
  9. package/dist/cache.d.cts +48 -4
  10. package/dist/cache.d.ts +48 -4
  11. package/dist/cache.js +177 -12
  12. package/dist/cache.js.map +1 -1
  13. package/dist/catalog-resolver.cjs +226 -51
  14. package/dist/catalog-resolver.cjs.map +1 -1
  15. package/dist/catalog-resolver.d.cts +41 -19
  16. package/dist/catalog-resolver.d.ts +41 -19
  17. package/dist/catalog-resolver.js +212 -46
  18. package/dist/catalog-resolver.js.map +1 -1
  19. package/dist/code-pattern-BP4zkNtT.d.cts +12 -0
  20. package/dist/code-pattern-BP4zkNtT.d.ts +12 -0
  21. package/dist/context.cjs +93 -0
  22. package/dist/context.cjs.map +1 -0
  23. package/dist/context.d.cts +19 -0
  24. package/dist/context.d.ts +19 -0
  25. package/dist/context.js +64 -0
  26. package/dist/context.js.map +1 -0
  27. package/dist/countries.cjs +1559 -0
  28. package/dist/countries.cjs.map +1 -0
  29. package/dist/countries.d.cts +13 -0
  30. package/dist/countries.d.ts +13 -0
  31. package/dist/countries.js +1532 -0
  32. package/dist/countries.js.map +1 -0
  33. package/dist/data-table.cjs +1280 -0
  34. package/dist/data-table.cjs.map +1 -0
  35. package/dist/data-table.d.cts +123 -0
  36. package/dist/data-table.d.ts +123 -0
  37. package/dist/data-table.js +1260 -0
  38. package/dist/data-table.js.map +1 -0
  39. package/dist/database.cjs +1510 -0
  40. package/dist/database.cjs.map +1 -0
  41. package/dist/database.d.cts +287 -0
  42. package/dist/database.d.ts +287 -0
  43. package/dist/database.js +1469 -0
  44. package/dist/database.js.map +1 -0
  45. package/dist/decimal.d.cts +0 -1
  46. package/dist/decimal.d.ts +0 -1
  47. package/dist/decorators.cjs +1817 -0
  48. package/dist/decorators.cjs.map +1 -0
  49. package/dist/decorators.d.cts +24 -0
  50. package/dist/decorators.d.ts +24 -0
  51. package/dist/decorators.js +1780 -0
  52. package/dist/decorators.js.map +1 -0
  53. package/dist/drizzle-pg-core.cjs +25 -0
  54. package/dist/drizzle-pg-core.cjs.map +1 -1
  55. package/dist/drizzle-pg-core.d.cts +7 -0
  56. package/dist/drizzle-pg-core.d.ts +7 -0
  57. package/dist/drizzle-pg-core.js +22 -0
  58. package/dist/drizzle-pg-core.js.map +1 -1
  59. package/dist/exceptions.cjs +301 -0
  60. package/dist/exceptions.cjs.map +1 -0
  61. package/dist/exceptions.d.cts +90 -0
  62. package/dist/exceptions.d.ts +90 -0
  63. package/dist/exceptions.js +257 -0
  64. package/dist/exceptions.js.map +1 -0
  65. package/dist/filter.types-PuARU9XD.d.cts +50 -0
  66. package/dist/filter.types-PuARU9XD.d.ts +50 -0
  67. package/dist/filters.cjs +407 -0
  68. package/dist/filters.cjs.map +1 -0
  69. package/dist/filters.d.cts +55 -0
  70. package/dist/filters.d.ts +55 -0
  71. package/dist/filters.js +377 -0
  72. package/dist/filters.js.map +1 -0
  73. package/dist/index.cjs +0 -5423
  74. package/dist/index.cjs.map +1 -1
  75. package/dist/index.d.cts +93 -854
  76. package/dist/index.d.ts +93 -854
  77. package/dist/index.js +0 -5301
  78. package/dist/index.js.map +1 -1
  79. package/dist/is-country.decorator-CcNwSSr5.d.cts +5 -0
  80. package/dist/is-country.decorator-CcNwSSr5.d.ts +5 -0
  81. package/dist/license.d.cts +1 -1
  82. package/dist/license.d.ts +1 -1
  83. package/dist/lodash.cjs.map +1 -1
  84. package/dist/lodash.js.map +1 -1
  85. package/dist/logger.cjs +862 -0
  86. package/dist/logger.cjs.map +1 -0
  87. package/dist/logger.d.cts +118 -0
  88. package/dist/logger.d.ts +118 -0
  89. package/dist/logger.js +816 -0
  90. package/dist/logger.js.map +1 -0
  91. package/dist/migrate.cjs +2 -2
  92. package/dist/migrate.cjs.map +1 -1
  93. package/dist/migrate.d.cts +2 -0
  94. package/dist/migrate.d.ts +2 -0
  95. package/dist/migrate.js +2 -2
  96. package/dist/migrate.js.map +1 -1
  97. package/dist/money.cjs +18 -3
  98. package/dist/money.cjs.map +1 -1
  99. package/dist/money.d.cts +13 -13
  100. package/dist/money.d.ts +13 -13
  101. package/dist/money.js +18 -3
  102. package/dist/money.js.map +1 -1
  103. package/dist/nats.cjs +34 -35
  104. package/dist/nats.cjs.map +1 -1
  105. package/dist/nats.d.cts +14 -14
  106. package/dist/nats.d.ts +14 -14
  107. package/dist/nats.js +32 -33
  108. package/dist/nats.js.map +1 -1
  109. package/dist/root.cjs +225 -0
  110. package/dist/root.cjs.map +1 -0
  111. package/dist/root.d.cts +4 -0
  112. package/dist/root.d.ts +4 -0
  113. package/dist/root.js +200 -0
  114. package/dist/root.js.map +1 -0
  115. package/dist/{types-DQRwj5rr.d.cts → types-C9jPdQqH.d.cts} +32 -6
  116. package/dist/{types-DQRwj5rr.d.ts → types-C9jPdQqH.d.ts} +32 -6
  117. package/dist/types.cjs +19 -0
  118. package/dist/types.cjs.map +1 -0
  119. package/dist/types.d.cts +17 -0
  120. package/dist/types.d.ts +17 -0
  121. package/dist/types.js +1 -0
  122. package/dist/types.js.map +1 -0
  123. package/dist/utils.cjs +273 -0
  124. package/dist/utils.cjs.map +1 -0
  125. package/dist/utils.d.cts +8 -0
  126. package/dist/utils.d.ts +8 -0
  127. package/dist/utils.js +245 -0
  128. package/dist/utils.js.map +1 -0
  129. package/package.json +122 -1
  130. package/dist/cache.service-_SuJIcNA.d.cts +0 -20
  131. package/dist/cache.service-_SuJIcNA.d.ts +0 -20
  132. package/dist/fastify-augmentation-CQPBNS5a.d.cts +0 -105
  133. package/dist/fastify-augmentation-CQPBNS5a.d.ts +0 -105
@@ -1,26 +1,126 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
+ // src/catalog-resolver/permission-deps.ts
5
+ function buildDependsMap(permissions) {
6
+ const present = new Set(permissions.map((p) => p.code));
7
+ const map = /* @__PURE__ */ new Map();
8
+ for (const p of permissions) {
9
+ map.set(p.code, (p.dependsOn ?? []).filter((dep) => dep !== p.code && present.has(dep)));
10
+ }
11
+ return map;
12
+ }
13
+ __name(buildDependsMap, "buildDependsMap");
14
+ function prereqClosure(code, deps) {
15
+ const out = /* @__PURE__ */ new Set();
16
+ const seen = /* @__PURE__ */ new Set([
17
+ code
18
+ ]);
19
+ const stack = [
20
+ code
21
+ ];
22
+ while (stack.length > 0) {
23
+ const current = stack.pop();
24
+ for (const dep of deps.get(current) ?? []) {
25
+ if (seen.has(dep)) continue;
26
+ seen.add(dep);
27
+ out.add(dep);
28
+ stack.push(dep);
29
+ }
30
+ }
31
+ return [
32
+ ...out
33
+ ];
34
+ }
35
+ __name(prereqClosure, "prereqClosure");
36
+ function cascadeLocked(codes, directlyLocked, deps) {
37
+ const locked = /* @__PURE__ */ new Set();
38
+ const visiting = /* @__PURE__ */ new Set();
39
+ const check = /* @__PURE__ */ __name((code) => {
40
+ if (locked.has(code)) return true;
41
+ if (directlyLocked.has(code)) {
42
+ locked.add(code);
43
+ return true;
44
+ }
45
+ if (visiting.has(code)) return false;
46
+ visiting.add(code);
47
+ const viaDep = (deps.get(code) ?? []).some(check);
48
+ visiting.delete(code);
49
+ if (viaDep) locked.add(code);
50
+ return viaDep;
51
+ }, "check");
52
+ for (const code of codes) check(code);
53
+ return locked;
54
+ }
55
+ __name(cascadeLocked, "cascadeLocked");
56
+ function filterGrantedByDeps(granted, deps) {
57
+ const ok = /* @__PURE__ */ new Set();
58
+ const visiting = /* @__PURE__ */ new Set();
59
+ const check = /* @__PURE__ */ __name((code) => {
60
+ if (ok.has(code)) return true;
61
+ if (!granted.has(code)) return false;
62
+ if (visiting.has(code)) return true;
63
+ visiting.add(code);
64
+ const satisfied = (deps.get(code) ?? []).every(check);
65
+ visiting.delete(code);
66
+ if (satisfied) ok.add(code);
67
+ return satisfied;
68
+ }, "check");
69
+ for (const code of granted) check(code);
70
+ return ok;
71
+ }
72
+ __name(filterGrantedByDeps, "filterGrantedByDeps");
73
+
74
+ // src/catalog-resolver/types.ts
75
+ var PLATFORMS = [
76
+ "web",
77
+ "mobile"
78
+ ];
79
+ var SITE_TYPES = [
80
+ "OUTLET",
81
+ "WAREHOUSE",
82
+ "PRODUCTION"
83
+ ];
84
+ function snapshotFeatureKey(code, scope) {
85
+ return `${scope}.${code}`;
86
+ }
87
+ __name(snapshotFeatureKey, "snapshotFeatureKey");
88
+ var SNAPSHOT_SCHEMA_VERSION = 2;
89
+
4
90
  // src/catalog-resolver/catalog.builder.ts
5
- function buildBuCatalog(snapshot, businessCode, planCode, buLocks, bucket) {
91
+ function featureAppliesAtNode(applicableSiteTypes, siteType) {
92
+ return applicableSiteTypes.includes(siteType);
93
+ }
94
+ __name(featureAppliesAtNode, "featureAppliesAtNode");
95
+ function findFeatureByCode(snapshot, code) {
96
+ for (const feature of Object.values(snapshot.features ?? {})) {
97
+ if (feature?.code === code) return feature;
98
+ }
99
+ return void 0;
100
+ }
101
+ __name(findFeatureByCode, "findFeatureByCode");
102
+ function buildSiteCatalog(snapshot, businessCode, planCode, siteLocks, bucket, siteType, scope) {
6
103
  if (!businessCode) return [];
7
104
  const business = snapshot.businesses?.[businessCode];
8
105
  if (!business) return [];
9
106
  const plan = planCode ? business.plans?.[planCode] : void 0;
10
107
  const plans = business.plans ?? {};
11
108
  const catalog = [];
12
- for (const app of business.apps) {
13
- const businessAppFeatures = app.features.map((code) => snapshot.features?.[code]).filter((f) => !!f && !!(f.microfrontends?.web || f.microfrontends?.mobile));
109
+ const sortedApps = [
110
+ ...business.apps
111
+ ].sort((a, b) => a.name.localeCompare(b.name));
112
+ for (const app of sortedApps) {
113
+ const businessAppFeatures = app.features.filter((ref) => scope === void 0 || ref.scope === scope).map((ref) => snapshot.features?.[snapshotFeatureKey(ref.code, ref.scope)]).filter((f) => !!f && !!(f.microfrontends?.web || f.microfrontends?.mobile) && (siteType === void 0 || featureAppliesAtNode(f.applicableSiteTypes, siteType)));
14
114
  if (businessAppFeatures.length === 0) continue;
15
115
  for (const feature of businessAppFeatures) {
16
116
  const membership = plan?.unlockedPermissions?.[feature.code];
17
117
  const web = feature.microfrontends?.web;
18
118
  const mobile = feature.microfrontends?.mobile;
19
- const permissions = buildPermissions(feature, businessCode, membership, buLocks, plans, bucket);
119
+ const permissions = buildPermissions(feature, businessCode, membership, siteLocks, plans, bucket);
20
120
  const memberOnBucket = membership?.[bucket] !== void 0;
21
- const buPlatformLocked = buLocks?.[feature.code]?.[bucket] === null;
22
- const locked = !memberOnBucket || buPlatformLocked;
23
- const lockReason = !memberOnBucket ? "PLAN" : buPlatformLocked ? "BU" : null;
121
+ const sitePlatformLocked = siteLocks?.[feature.code]?.[bucket] === null;
122
+ const locked = !memberOnBucket || sitePlatformLocked;
123
+ const lockReason = !memberOnBucket ? "PLAN" : sitePlatformLocked ? "SITE" : null;
24
124
  const unlockPlans = lockReason === "PLAN" ? plansIncludingFeature(plans, feature.code, bucket) : [];
25
125
  catalog.push({
26
126
  code: feature.code,
@@ -52,25 +152,43 @@ function buildBuCatalog(snapshot, businessCode, planCode, buLocks, bucket) {
52
152
  }
53
153
  return catalog;
54
154
  }
55
- __name(buildBuCatalog, "buildBuCatalog");
155
+ __name(buildSiteCatalog, "buildSiteCatalog");
56
156
  function isPlanMember(entry) {
57
157
  return !!entry && (entry.web !== void 0 || entry.mobile !== void 0);
58
158
  }
59
159
  __name(isPlanMember, "isPlanMember");
60
- function isBuLockedOnPlatform(entry, platform, code) {
160
+ function isSiteLockedOnPlatform(entry, platform, code) {
61
161
  const locks = entry?.[platform];
62
162
  return locks === null || (locks?.includes(code) ?? false);
63
163
  }
64
- __name(isBuLockedOnPlatform, "isBuLockedOnPlatform");
65
- function buildPermissions(feature, businessCode, planMembership, buLocks, plans, bucket) {
164
+ __name(isSiteLockedOnPlatform, "isSiteLockedOnPlatform");
165
+ function buildPermissions(feature, businessCode, planMembership, siteLocks, plans, bucket) {
66
166
  const planUnlocked = new Set(planMembership?.[bucket] ?? []);
67
- const lockEntry = buLocks?.[feature.code];
68
- return (feature.permissions ?? []).filter((p) => p.isGlobal || p.businesses.includes(businessCode)).map((p) => {
69
- const planAllows = planUnlocked.has(p.code);
70
- const buAllows = !isBuLockedOnPlatform(lockEntry, bucket, p.code);
71
- const locked = !planAllows || !buAllows;
72
- const lockReason = !planAllows ? "PLAN" : !buAllows ? "BU" : null;
73
- const unlockPlans = lockReason === "PLAN" ? plansUnlockingPermission(plans, feature.code, p.code, bucket) : [];
167
+ const lockEntry = siteLocks?.[feature.code];
168
+ const perms = (feature.permissions ?? []).filter((p) => p.isGlobal || p.businesses.includes(businessCode));
169
+ const deps = buildDependsMap(perms);
170
+ const codes = perms.map((p) => p.code);
171
+ const directlyPlanLocked = /* @__PURE__ */ new Set();
172
+ const directlySiteLocked = /* @__PURE__ */ new Set();
173
+ for (const p of perms) {
174
+ if (!planUnlocked.has(p.code)) directlyPlanLocked.add(p.code);
175
+ if (isSiteLockedOnPlatform(lockEntry, bucket, p.code)) directlySiteLocked.add(p.code);
176
+ }
177
+ const directlyLocked = /* @__PURE__ */ new Set([
178
+ ...directlyPlanLocked,
179
+ ...directlySiteLocked
180
+ ]);
181
+ const lockedSet = cascadeLocked(codes, directlyLocked, deps);
182
+ return perms.map((p) => {
183
+ const locked = lockedSet.has(p.code);
184
+ const closure = [
185
+ p.code,
186
+ ...prereqClosure(p.code, deps)
187
+ ];
188
+ const planReason = closure.some((c) => directlyPlanLocked.has(c));
189
+ const siteReason = closure.some((c) => directlySiteLocked.has(c));
190
+ const lockReason = !locked ? null : planReason ? "PLAN" : siteReason ? "SITE" : null;
191
+ const unlockPlans = locked && lockReason === "PLAN" ? plansUnlockingClosure(plans, feature.code, closure, bucket) : [];
74
192
  return {
75
193
  code: p.code,
76
194
  locked,
@@ -80,14 +198,15 @@ function buildPermissions(feature, businessCode, planMembership, buLocks, plans,
80
198
  });
81
199
  }
82
200
  __name(buildPermissions, "buildPermissions");
83
- function plansUnlockingPermission(plans, featureCode, permCode, bucket) {
201
+ function plansUnlockingClosure(plans, featureCode, closure, bucket) {
84
202
  const result = [];
85
203
  for (const [code, plan] of Object.entries(plans)) {
86
- if (plan.unlockedPermissions?.[featureCode]?.[bucket]?.includes(permCode)) result.push(code);
204
+ const unlocked = plan.unlockedPermissions?.[featureCode]?.[bucket];
205
+ if (unlocked && closure.every((c) => unlocked.includes(c))) result.push(code);
87
206
  }
88
207
  return result;
89
208
  }
90
- __name(plansUnlockingPermission, "plansUnlockingPermission");
209
+ __name(plansUnlockingClosure, "plansUnlockingClosure");
91
210
  function plansIncludingFeature(plans, featureCode, bucket) {
92
211
  const result = [];
93
212
  for (const [code, plan] of Object.entries(plans)) {
@@ -96,23 +215,24 @@ function plansIncludingFeature(plans, featureCode, bucket) {
96
215
  return result;
97
216
  }
98
217
  __name(plansIncludingFeature, "plansIncludingFeature");
99
- function buildBuRoles(snapshot, businessCode) {
218
+ function buildSiteRoles(snapshot, businessCode) {
100
219
  if (!businessCode) return [];
101
220
  const business = snapshot.businesses?.[businessCode];
102
221
  if (!business) return [];
103
222
  return Object.values(business.roleTemplates ?? {});
104
223
  }
105
- __name(buildBuRoles, "buildBuRoles");
224
+ __name(buildSiteRoles, "buildSiteRoles");
106
225
 
107
- // src/catalog-resolver/types.ts
108
- var PLATFORMS = [
109
- "web",
110
- "mobile"
111
- ];
112
- var SNAPSHOT_SCHEMA_VERSION = 1;
113
-
114
- // src/catalog-resolver/bu-matrix.builder.ts
115
- function buildBuMatrix(snapshot, businessCode, planCode, buLocks) {
226
+ // src/catalog-resolver/site-matrix.builder.ts
227
+ function buildSiteMatrix(snapshot, businessCode, planCode, siteLocks, siteType) {
228
+ return buildMatrix(snapshot, businessCode, planCode, siteLocks, false, siteType);
229
+ }
230
+ __name(buildSiteMatrix, "buildSiteMatrix");
231
+ function buildPlanMatrix(snapshot, businessCode, planCode, siteLocks) {
232
+ return buildMatrix(snapshot, businessCode, planCode, siteLocks, true);
233
+ }
234
+ __name(buildPlanMatrix, "buildPlanMatrix");
235
+ function buildMatrix(snapshot, businessCode, planCode, siteLocks, allScopes, siteType) {
116
236
  const business = businessCode ? snapshot.businesses?.[businessCode] : void 0;
117
237
  const plans = business?.plans ?? {};
118
238
  const plan = planCode ? plans[planCode] : void 0;
@@ -120,7 +240,7 @@ function buildBuMatrix(snapshot, businessCode, planCode, buLocks) {
120
240
  code: planCode ?? "",
121
241
  name: plan?.name ?? planCode ?? ""
122
242
  };
123
- const locks = buLocks ?? {};
243
+ const locks = siteLocks ?? {};
124
244
  if (!business || !plan) return {
125
245
  plan: planMeta,
126
246
  apps: [],
@@ -131,20 +251,23 @@ function buildBuMatrix(snapshot, businessCode, planCode, buLocks) {
131
251
  let unlockedCount = 0;
132
252
  let totalCount = 0;
133
253
  const features = [];
134
- for (const code of app.features) {
135
- const feature = snapshot.features?.[code];
254
+ for (const ref of app.features) {
255
+ if (!allScopes && ref.scope !== "SITE") continue;
256
+ const code = ref.code;
257
+ const feature = snapshot.features?.[snapshotFeatureKey(code, ref.scope)];
136
258
  if (!feature) continue;
259
+ if (siteType !== void 0 && !featureAppliesAtNode(feature.applicableSiteTypes, siteType)) continue;
137
260
  const platforms = PLATFORMS.filter((p) => !!feature.microfrontends?.[p]);
138
261
  if (platforms.length === 0) continue;
139
262
  const membership = plan.unlockedPermissions?.[code];
140
263
  const featureInPlan = isPlanMember(membership);
141
- const buEntry = buLocks?.[code];
264
+ const siteEntry = siteLocks?.[code];
142
265
  const permissions = (feature.permissions ?? []).filter((p) => p.isGlobal || p.businesses.includes(businessCode ?? "")).map((p) => {
143
266
  const cell = /* @__PURE__ */ __name((plat) => {
144
267
  if (!platforms.includes(plat)) return null;
145
268
  const planCodes = membership?.[plat];
146
269
  const inPlan = featureInPlan && planCodes !== void 0 && planCodes.includes(p.code);
147
- const selected = inPlan && !isBuLockedOnPlatform(buEntry, plat, p.code);
270
+ const selected = inPlan && !isSiteLockedOnPlatform(siteEntry, plat, p.code);
148
271
  const availableIn = inPlan ? [] : plansUnlockingPerm(plans, code, p.code, plat, planCode);
149
272
  totalCount += 1;
150
273
  if (inPlan) unlockedCount += 1;
@@ -157,6 +280,7 @@ function buildBuMatrix(snapshot, businessCode, planCode, buLocks) {
157
280
  return {
158
281
  code: p.code,
159
282
  label: p.label,
283
+ dependsOn: p.dependsOn ?? [],
160
284
  web: cell("web"),
161
285
  mobile: cell("mobile")
162
286
  };
@@ -165,12 +289,15 @@ function buildBuMatrix(snapshot, businessCode, planCode, buLocks) {
165
289
  code: feature.code,
166
290
  name: feature.name,
167
291
  icon: feature.lucideIcon ?? null,
292
+ scope: feature.scope,
293
+ applicableSiteTypes: feature.applicableSiteTypes,
168
294
  platforms,
169
295
  inPlan: featureInPlan,
170
296
  availableIn: featureInPlan ? [] : plansIncludingFeature2(plans, code, planCode),
171
297
  permissions
172
298
  });
173
299
  }
300
+ if (features.length === 0) continue;
174
301
  apps.push({
175
302
  code: app.code,
176
303
  name: app.name,
@@ -180,13 +307,14 @@ function buildBuMatrix(snapshot, businessCode, planCode, buLocks) {
180
307
  features
181
308
  });
182
309
  }
310
+ apps.sort((a, b) => a.name.localeCompare(b.name));
183
311
  return {
184
312
  plan: planMeta,
185
313
  apps,
186
314
  locks
187
315
  };
188
316
  }
189
- __name(buildBuMatrix, "buildBuMatrix");
317
+ __name(buildMatrix, "buildMatrix");
190
318
  function plansUnlockingPerm(plans, featureCode, permCode, platform, excludeCode) {
191
319
  const names = [];
192
320
  for (const [code, p] of Object.entries(plans)) {
@@ -245,13 +373,35 @@ __name(composeRoleGrants, "composeRoleGrants");
245
373
 
246
374
  // src/catalog-resolver/resolve-user-features.ts
247
375
  function resolveUserFeatures(params) {
248
- const { snapshot, businessCode, planCode, buLocks, roleFeatures, platform } = params;
376
+ const { snapshot, businessCode, planCode, siteLocks, roleFeatures, platform, siteType, scope } = params;
249
377
  const bucket = platform === "web" ? "web" : "mobile";
250
- const catalog = buildBuCatalog(snapshot, businessCode, planCode, buLocks, bucket);
378
+ const featureByCode = /* @__PURE__ */ __name((code) => scope ? snapshot.features?.[snapshotFeatureKey(code, scope)] : findFeatureByCode(snapshot, code), "featureByCode");
379
+ const catalog = buildSiteCatalog(snapshot, businessCode, planCode, siteLocks, bucket, siteType, scope);
251
380
  const catalogMap = new Map(catalog.map((f) => [
252
381
  f.code,
253
382
  f
254
383
  ]));
384
+ const businessPlans = snapshot.businesses[businessCode]?.plans ?? {};
385
+ const currentUnlockedCodes = /* @__PURE__ */ new Set();
386
+ if (planCode && businessPlans[planCode]) {
387
+ for (const [featureCode, platforms] of Object.entries(businessPlans[planCode].unlockedPermissions ?? {})) {
388
+ if (platforms?.[bucket] !== void 0) currentUnlockedCodes.add(featureCode);
389
+ }
390
+ }
391
+ const planAdds = /* @__PURE__ */ new Map();
392
+ for (const [planKey, plan] of Object.entries(businessPlans)) {
393
+ if (planKey === planCode) continue;
394
+ const adds = [];
395
+ for (const [featureCode, platforms] of Object.entries(plan.unlockedPermissions ?? {})) {
396
+ if (platforms?.[bucket] === void 0 || currentUnlockedCodes.has(featureCode)) continue;
397
+ const name = featureByCode(featureCode)?.name;
398
+ if (name) adds.push({
399
+ code: featureCode,
400
+ name
401
+ });
402
+ }
403
+ planAdds.set(planKey, adds);
404
+ }
255
405
  const grantedFeatures = /* @__PURE__ */ new Map();
256
406
  for (const [code, grant] of Object.entries(roleFeatures ?? {})) {
257
407
  const granted = grant?.[bucket];
@@ -265,18 +415,23 @@ function resolveUserFeatures(params) {
265
415
  if (!catalogEntry) continue;
266
416
  const route = pickRouteForPlatform(catalogEntry, platform);
267
417
  if (!route) continue;
418
+ const featureDeps = buildDependsMap(featureByCode(code)?.permissions ?? []);
268
419
  const permByCode = new Map((catalogEntry.permissions ?? []).map((p) => [
269
420
  p.code,
270
421
  p
271
422
  ]));
272
423
  const grantedPerms = [
273
- ...permsSet
424
+ ...filterGrantedByDeps(permsSet, featureDeps)
274
425
  ];
275
426
  const lockedPermissions = grantedPerms.map((c) => permByCode.get(c)).filter((p) => !!p?.locked).map((p) => ({
276
427
  code: p.code,
277
428
  reason: p.lockReason ?? null,
278
429
  unlockPlans: p.unlockPlans ?? []
279
430
  }));
431
+ const upsell = catalogEntry.locked && catalogEntry.lockReason === "PLAN" ? (catalogEntry.unlockPlans ?? []).map((plan) => ({
432
+ plan,
433
+ features: (planAdds.get(plan) ?? []).filter((f) => f.code !== code).map((f) => f.name)
434
+ })).filter((group) => group.features.length > 0) : [];
280
435
  features.push({
281
436
  code,
282
437
  name: catalogEntry.name,
@@ -288,6 +443,7 @@ function resolveUserFeatures(params) {
288
443
  lockReason: catalogEntry.lockReason ?? null,
289
444
  unlockPlans: catalogEntry.unlockPlans ?? [],
290
445
  lockedPermissions,
446
+ upsell,
291
447
  route,
292
448
  appCode: catalogEntry.appCode,
293
449
  appName: catalogEntry.appName,
@@ -295,6 +451,7 @@ function resolveUserFeatures(params) {
295
451
  appSortOrder: catalogEntry.appSortOrder
296
452
  });
297
453
  }
454
+ features.sort((a, b) => a.appName.localeCompare(b.appName));
298
455
  return features;
299
456
  }
300
457
  __name(resolveUserFeatures, "resolveUserFeatures");
@@ -317,14 +474,23 @@ function pickRouteForPlatform(entry, platform) {
317
474
  __name(pickRouteForPlatform, "pickRouteForPlatform");
318
475
  export {
319
476
  PLATFORMS,
477
+ SITE_TYPES,
320
478
  SNAPSHOT_SCHEMA_VERSION,
321
- buildBuCatalog,
322
- buildBuMatrix,
323
- buildBuRoles,
479
+ buildDependsMap,
480
+ buildPlanMatrix,
481
+ buildSiteCatalog,
482
+ buildSiteMatrix,
483
+ buildSiteRoles,
484
+ cascadeLocked,
324
485
  composeRoleGrants,
325
- isBuLockedOnPlatform,
486
+ featureAppliesAtNode,
487
+ filterGrantedByDeps,
488
+ findFeatureByCode,
326
489
  isPlanMember,
490
+ isSiteLockedOnPlatform,
327
491
  pickRouteForPlatform,
328
- resolveUserFeatures
492
+ prereqClosure,
493
+ resolveUserFeatures,
494
+ snapshotFeatureKey
329
495
  };
330
496
  //# sourceMappingURL=catalog-resolver.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/catalog-resolver/catalog.builder.ts","../src/catalog-resolver/types.ts","../src/catalog-resolver/bu-matrix.builder.ts","../src/catalog-resolver/compose-role-grants.ts","../src/catalog-resolver/resolve-user-features.ts"],"sourcesContent":["import type {\n BuFeatureLocks,\n CatalogPermission,\n FeatureCatalogEntry,\n LockReason,\n PlatformBucket,\n PlatformCodes,\n RoleItem,\n SnapshotFeature,\n SnapshotPlan,\n VersionSnapshot,\n} from './types';\n\n// Builds the per-BU catalog for ONE platform bucket. The plan is the ceiling (membership + unlocks); buLocks is a\n// deny-list within the plan (undefined or absent entries ⇒ fully available). Each permission carries\n// locked + lockReason + unlockPlans, all evaluated against the requested bucket only.\nexport function buildBuCatalog(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n buLocks: BuFeatureLocks | undefined,\n bucket: PlatformBucket,\n): FeatureCatalogEntry[] {\n if (!businessCode) return [];\n const business = snapshot.businesses?.[businessCode];\n if (!business) return [];\n const plan = planCode ? business.plans?.[planCode] : undefined;\n const plans = business.plans ?? {};\n\n const catalog: FeatureCatalogEntry[] = [];\n for (const app of business.apps) {\n // The app's renderable features (each feature pins to exactly one app)\n const businessAppFeatures = app.features\n .map((code) => snapshot.features?.[code])\n .filter((f): f is SnapshotFeature => !!f && !!(f.microfrontends?.web || f.microfrontends?.mobile));\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 (rendered as a locked\n // tile with an upsell) instead of vanishing. Core filters the catalog down to what the user's role grants.\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. A role grant on a\n // plan-omitted (feature or platform) still resolves as a locked tile with an upsell instead of vanishing.\n const web = feature.microfrontends?.web;\n const mobile = feature.microfrontends?.mobile;\n\n const permissions = buildPermissions(feature, businessCode, membership, buLocks, plans, bucket);\n // Feature-level lock is EXPLICIT, never derived from permission locks: the plan must include the feature\n // on this bucket (membership) and the BU must not null-lock the platform. Permission-code locks only\n // disable individual actions — the feature still renders (sidebar/tile stays enabled).\n const memberOnBucket = membership?.[bucket] !== undefined;\n const buPlatformLocked = buLocks?.[feature.code]?.[bucket] === null;\n const locked = !memberOnBucket || buPlatformLocked;\n const lockReason: LockReason | null = !memberOnBucket ? 'PLAN' : buPlatformLocked ? 'BU' : 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 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 return !!entry && (entry.web !== undefined || entry.mobile !== undefined);\n}\n\n// Per-platform BU-lock primitive: platform null locks the whole feature there; string[] locks those codes;\n// entry/platform absent ⇒ not locked\nexport function isBuLockedOnPlatform(\n entry: BuFeatureLocks[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 the BU deny-list.\n// Everything is bucket-scoped: a web resolution ignores mobile unlocks and mobile locks entirely.\nfunction buildPermissions(\n feature: SnapshotFeature,\n businessCode: string,\n planMembership: PlatformCodes | undefined,\n buLocks: BuFeatureLocks | undefined,\n plans: Record<string, SnapshotPlan>,\n bucket: PlatformBucket,\n): CatalogPermission[] {\n const planUnlocked = new Set(planMembership?.[bucket] ?? []);\n const lockEntry = buLocks?.[feature.code];\n\n return (feature.permissions ?? [])\n .filter((p) => p.isGlobal || p.businesses.includes(businessCode))\n .map((p) => {\n const planAllows = planUnlocked.has(p.code);\n const buAllows = !isBuLockedOnPlatform(lockEntry, bucket, p.code);\n const locked = !planAllows || !buAllows;\n // Plan is the ceiling, so a plan-lock wins over a BU-lock when reporting the reason\n const lockReason: LockReason | null = !planAllows ? 'PLAN' : !buAllows ? 'BU' : null;\n const unlockPlans = lockReason === 'PLAN' ? plansUnlockingPermission(plans, feature.code, p.code, bucket) : [];\n return { code: p.code, locked, lockReason, unlockPlans };\n });\n}\n\n// Plan codes (in the business) whose unlocked set includes this feature+permission on the bucket — upsell targets\nfunction plansUnlockingPermission(\n plans: Record<string, SnapshotPlan>,\n featureCode: string,\n permCode: 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]?.includes(permCode)) 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 buildBuRoles(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","// ——— Platform algebra ———\n// Everything in this domain (plan unlocks, role grants, BU locks) is stored per UI platform bucket.\n\nexport type PlatformBucket = 'web' | 'mobile';\n\nexport const PLATFORMS: PlatformBucket[] = ['web', 'mobile'];\n\n// Allow entry: a bucket key present (even []) = membership on that platform; the array holds permission codes\nexport interface PlatformCodes {\n web?: string[];\n mobile?: string[];\n}\n\n// Deny entry: null locks/revokes the whole bucket, string[] the listed codes, absent = untouched\nexport interface PlatformDenyCodes {\n web?: string[] | null;\n mobile?: string[] | null;\n}\n\n// Code-keyed allow-list — plan unlocks, role grants, and matrix selections all share this exact shape\nexport type FeatureUnlocks = Record<string, PlatformCodes>;\n\n// BU deny-list overlay (same shape as role revokes) — feature absent = fully available within the plan\nexport type BuFeatureLocks = Record<string, PlatformDenyCodes>;\n\n// ——— Snapshot document shape — what gets stored in versions.snapshot and signed into the catalog license ———\n\nexport interface SnapshotPermission {\n code: string;\n label: string;\n isGlobal: boolean;\n businesses: string[];\n}\n// WEB microfrontend route — a single remote entry. All non-null: a WEB microfrontend row is\n// CHECK-constrained to have remoteEntry set, and the feature↔MF link's module/route are NOT NULL.\nexport interface SnapshotMicrofrontendWeb {\n code: string;\n name: string;\n remoteEntry: string;\n exposedModule: string;\n routePrefix: string;\n}\n// MOBILE microfrontend route — per-OS remote entries (android + ios). All non-null: a MOBILE row is\n// CHECK-constrained to have both android + ios set, and the link's module/route are NOT NULL.\nexport interface SnapshotMicrofrontendMobile {\n code: string;\n name: string;\n remoteEntryAndroid: string;\n remoteEntryIos: string;\n exposedModule: string;\n routePrefix: string;\n}\n// Per-platform microfrontend routes — mirrors the PlatformCodes {web?, mobile?} bucket shape\nexport interface SnapshotMicrofrontends {\n web?: SnapshotMicrofrontendWeb;\n mobile?: SnapshotMicrofrontendMobile;\n}\nexport interface SnapshotFeature {\n code: string;\n name: string;\n lucideIcon: string;\n sfSymbol: string;\n materialSymbol: string;\n permissions: SnapshotPermission[];\n microfrontends: SnapshotMicrofrontends;\n}\nexport interface SnapshotApp {\n code: string;\n name: string;\n icon: string;\n sortOrder: number;\n features: string[];\n}\n// Also the shape provisioned to core as a role stub (the template's code is the durable org-role link)\nexport interface SnapshotRoleTemplate {\n name: string;\n code: string;\n // featureCode -> per-platform granted permission codes\n features: FeatureUnlocks;\n}\n// A plan is a lock overlay: the feature-permissions it UNLOCKS per platform (everything else renders locked).\n// Apps are derived from these.\nexport interface SnapshotPlan {\n code: string;\n name: string;\n isCustom: boolean;\n maxBusinessUnits: number | null;\n unlockedPermissions: FeatureUnlocks;\n}\nexport interface SnapshotBusiness {\n name: string;\n apps: SnapshotApp[];\n // Keyed by template code — the same key org roles link to\n roleTemplates: Record<string, SnapshotRoleTemplate>;\n plans: Record<string, SnapshotPlan>;\n}\nexport interface VersionSnapshot {\n // Shape revision hook for future migrations (absent = pre-versioned snapshots)\n schemaVersion?: number;\n features: Record<string, SnapshotFeature>;\n businesses: Record<string, SnapshotBusiness>;\n}\n\n// The current snapshot shape revision written by the cloud snapshot builder\nexport const SNAPSHOT_SCHEMA_VERSION = 1;\n\n// Why a permission/feature is locked: PLAN (org's plan doesn't unlock it) or BU (this BU restricts it)\nexport type LockReason = 'PLAN' | 'BU';\n\n// A catalog permission: its lock state + reason + (when plan-locked) the plans that would unlock it (upsell)\nexport interface CatalogPermission {\n code: string;\n locked: boolean;\n lockReason: LockReason | null;\n unlockPlans: string[];\n}\n\n// One resolved feature of the per-BU catalog\nexport interface FeatureCatalogEntry {\n code: string;\n name: string;\n lucideIcon: string | null;\n sfSymbol: string;\n materialSymbol: string;\n // Per-platform MF route — symmetric { web?, mobile? }; null when the feature has no MF for that platform.\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 // Feature-level lock is explicit — plan non-membership or a BU whole-platform lock (never derived from\n // permission-code locks, which only disable individual actions)\n locked: boolean;\n lockReason: LockReason | null;\n // Plans (in the business) that would unlock the feature when it's plan-locked — for upsell\n unlockPlans: string[];\n permissions: CatalogPermission[];\n}\n\n// Role template pushed to core for provisioning — identical to the snapshot template shape\nexport type RoleItem = SnapshotRoleTemplate;\n","import { isBuLockedOnPlatform, isPlanMember } from './catalog.builder';\nimport { type BuFeatureLocks, PLATFORMS, type PlatformBucket, type SnapshotPlan, type VersionSnapshot } from './types';\n\n// A single (permission, platform) cell. null ⇒ the feature doesn't ship on that platform.\nexport interface BuMatrixCell {\n inPlan: boolean; // the org's plan unlocks this (feature, platform, permission)\n selected: boolean; // the BU currently has it enabled (never true when !inPlan)\n availableIn: string[]; // other plan names that unlock it — upsell; only when !inPlan\n}\n\nexport interface BuMatrixPermission {\n code: string;\n label: string;\n web: BuMatrixCell | null;\n mobile: BuMatrixCell | null;\n}\n\nexport interface BuMatrixFeature {\n code: string;\n name: string;\n icon: string | null;\n platforms: PlatformBucket[]; // platforms the feature ships on\n inPlan: boolean; // FEATURE-level: is this feature a member of the org's plan at all?\n availableIn: string[]; // plans that include the feature — upsell when !inPlan\n permissions: BuMatrixPermission[];\n}\n\nexport interface BuMatrixApp {\n code: string;\n name: string;\n icon: string | null;\n unlockedCount: number; // (feature × platform × permission) cells the plan unlocks\n totalCount: number; // all possible cells in this app\n features: BuMatrixFeature[];\n}\n\nexport interface BuMatrix {\n plan: { code: string; name: string };\n apps: BuMatrixApp[];\n // The stored BU deny-list verbatim — lock editors seed from this, never from derived selected flags\n locks: BuFeatureLocks;\n}\n\n// Builds the full apps/features/permissions matrix from the version snapshot — NOT filtered to plan members.\n// Every app/feature/permission renders; plan-locked items carry inPlan=false + availableIn (the upsell plans).\n// buLocks layers the BU's deny-list within the plan (undefined or absent entries ⇒ the full plan is selected).\nexport function buildBuMatrix(\n snapshot: VersionSnapshot,\n businessCode: string | undefined,\n planCode: string | undefined,\n buLocks: BuFeatureLocks | undefined,\n): BuMatrix {\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 = buLocks ?? {};\n if (!business || !plan) return { plan: planMeta, apps: [], locks };\n\n const apps: BuMatrixApp[] = [];\n for (const app of business.apps) {\n let unlockedCount = 0;\n let totalCount = 0;\n const features: BuMatrixFeature[] = [];\n\n for (const code of app.features) {\n const feature = snapshot.features?.[code];\n if (!feature) continue;\n const platforms = PLATFORMS.filter((p) => !!feature.microfrontends?.[p]);\n if (platforms.length === 0) continue;\n\n const membership = plan.unlockedPermissions?.[code];\n const featureInPlan = isPlanMember(membership);\n const buEntry = buLocks?.[code];\n\n const permissions: BuMatrixPermission[] = (feature.permissions ?? [])\n .filter((p) => p.isGlobal || p.businesses.includes(businessCode ?? ''))\n .map((p) => {\n const cell = (plat: PlatformBucket): BuMatrixCell | null => {\n if (!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 BU locks it on this platform\n const selected = inPlan && !isBuLockedOnPlatform(buEntry, plat, p.code);\n const availableIn = inPlan ? [] : plansUnlockingPerm(plans, code, p.code, plat, planCode);\n totalCount += 1;\n if (inPlan) unlockedCount += 1;\n return { inPlan, selected, availableIn };\n };\n return { code: p.code, label: p.label, web: cell('web'), mobile: cell('mobile') };\n });\n\n features.push({\n code: feature.code,\n name: feature.name,\n icon: feature.lucideIcon ?? null,\n platforms,\n inPlan: featureInPlan,\n availableIn: featureInPlan ? [] : plansIncludingFeature(plans, code, planCode),\n permissions,\n });\n }\n\n apps.push({ code: app.code, name: app.name, icon: app.icon ?? null, unlockedCount, totalCount, features });\n }\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","import { type FeatureUnlocks, PLATFORMS, type PlatformCodes, type PlatformDenyCodes } from './types';\n\n// Grants removed from a base template — same deny algebra as BU locks\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 based custom role's effective grants: merge(base ∪ additions) − revoked (design doc §10).\n// Platform membership survives a code revoke (may leave [] = view-only); a null revoke removes the\n// platform entirely; a feature with no surviving platform disappears. 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 if (composed.web === undefined && composed.mobile === undefined) continue;\n result[code] = composed;\n }\n\n return result;\n}\n","import { buildBuCatalog } from './catalog.builder';\nimport type { BuFeatureLocks, FeatureUnlocks, LockReason, PlatformBucket, VersionSnapshot } from './types';\n\n// Client surface requesting resolution — web picks the WEB route block, ios/android pick the MOBILE block per-OS\nexport type ClientPlatform = 'web' | 'ios' | 'android';\n\n// A granted permission that is locked, with why and which plans would unlock it (for upsell)\nexport interface LockedPermission {\n code: string;\n reason: LockReason | null;\n unlockPlans: 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 // Lock overlay (for upsell): feature-level locked + reason + unlock plans, and the granted-but-locked permissions\n locked: boolean;\n lockReason: LockReason | null;\n unlockPlans: string[];\n lockedPermissions: LockedPermission[];\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 // undefined = nothing BU-locked (the full plan is available)\n buLocks: BuFeatureLocks | undefined;\n // The user's composed role grants: featureCode -> { web?: [permCode…], mobile?: [permCode…] }\n roleFeatures: FeatureUnlocks;\n platform: ClientPlatform;\n}\n\n// Resolves the features + MF config a user sees at a BU: plan ∧ BU catalog intersected with the role's grants.\n// platform picks which microfrontend block flows into PermissionFeature.route:\n// 'web' → WEB block (remoteEntry + exposedModule + routePrefix)\n// 'ios' → MOBILE block, remoteEntry = remoteEntryIos\n// 'android' → MOBILE block, remoteEntry = remoteEntryAndroid\n// Features missing the requested platform's block are filtered out.\nexport function resolveUserFeatures(params: ResolveUserFeaturesParams): PermissionFeature[] {\n const { snapshot, businessCode, planCode, buLocks, roleFeatures, platform } = params;\n\n // Plan unlocks, BU locks, and role grants are all stored per platform; resolve only the requesting\n // surface's bucket. web → 'web'; ios/android → 'mobile'. (platform still drives the per-OS route below.)\n const bucket: PlatformBucket = platform === 'web' ? 'web' : 'mobile';\n\n // Plan ∧ BU overlay for this bucket — emits EVERY business feature (plan non-members come out fully locked)\n const catalog = buildBuCatalog(snapshot, businessCode, planCode, buLocks, bucket);\n const catalogMap = new Map(catalog.map((f) => [f.code, f]));\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 const route = pickRouteForPlatform(catalogEntry, platform);\n // Feature isn't published to this platform — omit so client doesn't see an unloadable tile.\n if (!route) continue;\n\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 const grantedPerms = [...permsSet];\n const lockedPermissions: LockedPermission[] = grantedPerms\n .map((c) => permByCode.get(c))\n .filter((p): p is NonNullable<typeof p> => !!p?.locked)\n .map((p) => ({ code: p.code, reason: p.lockReason ?? null, unlockPlans: p.unlockPlans ?? [] }));\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 lockedPermissions,\n route,\n appCode: catalogEntry.appCode,\n appName: catalogEntry.appName,\n appIcon: catalogEntry.appIcon,\n appSortOrder: catalogEntry.appSortOrder,\n });\n }\n\n return features;\n}\n\n// Selects the route block from a catalog entry for the requested platform.\n// Returns null when the catalog entry doesn't publish to that platform.\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 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"],"mappings":"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","names":["buildBuCatalog","snapshot","businessCode","planCode","buLocks","bucket","business","businesses","plan","plans","undefined","catalog","app","apps","businessAppFeatures","features","map","code","filter","f","microfrontends","web","mobile","length","feature","membership","unlockedPermissions","permissions","buildPermissions","memberOnBucket","buPlatformLocked","locked","lockReason","unlockPlans","plansIncludingFeature","push","name","lucideIcon","sfSymbol","materialSymbol","remoteEntry","exposedModule","routePrefix","remoteEntryAndroid","remoteEntryIos","appCode","appName","appIcon","icon","appSortOrder","sortOrder","isPlanMember","entry","isBuLockedOnPlatform","platform","locks","includes","planMembership","planUnlocked","Set","lockEntry","p","isGlobal","planAllows","has","buAllows","plansUnlockingPermission","featureCode","permCode","result","Object","entries","buildBuRoles","values","roleTemplates","PLATFORMS","SNAPSHOT_SCHEMA_VERSION","buildBuMatrix","snapshot","businessCode","planCode","buLocks","business","businesses","undefined","plans","plan","planMeta","code","name","locks","apps","app","unlockedCount","totalCount","features","feature","platforms","PLATFORMS","filter","p","microfrontends","length","membership","unlockedPermissions","featureInPlan","isPlanMember","buEntry","permissions","isGlobal","includes","map","cell","plat","planCodes","inPlan","selected","isBuLockedOnPlatform","availableIn","plansUnlockingPerm","label","web","mobile","push","icon","lucideIcon","plansIncludingFeature","featureCode","permCode","platform","excludeCode","names","Object","entries","unionBucket","base","add","undefined","Set","composeRoleGrants","params","baseFeatures","additions","revoked","result","featureCodes","Object","keys","code","revokes","composed","bucket","PLATFORMS","merged","revoke","filter","c","includes","web","mobile","resolveUserFeatures","params","snapshot","businessCode","planCode","buLocks","roleFeatures","platform","bucket","catalog","buildBuCatalog","catalogMap","Map","map","f","code","grantedFeatures","grant","Object","entries","granted","undefined","has","set","Set","perm","get","add","features","permsSet","catalogEntry","route","pickRouteForPlatform","permByCode","permissions","p","grantedPerms","lockedPermissions","c","filter","locked","reason","lockReason","unlockPlans","push","name","lucideIcon","sfSymbol","materialSymbol","appCode","appName","appIcon","appSortOrder","entry","mobile","remoteEntry","remoteEntryIos","remoteEntryAndroid","exposedModule","routePrefix","web"]}
1
+ {"version":3,"sources":["../src/catalog-resolver/permission-deps.ts","../src/catalog-resolver/types.ts","../src/catalog-resolver/catalog.builder.ts","../src/catalog-resolver/site-matrix.builder.ts","../src/catalog-resolver/compose-role-grants.ts","../src/catalog-resolver/resolve-user-features.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 UI platform bucket ———\n\nexport type PlatformBucket = 'web' | 'mobile';\n\nexport const PLATFORMS: PlatformBucket[] = ['web', 'mobile'];\n\nexport interface PlatformCodes {\n web?: string[];\n mobile?: string[];\n}\n\nexport interface PlatformDenyCodes {\n web?: string[] | null;\n mobile?: 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 SnapshotPermission {\n code: string;\n label: string;\n isGlobal: boolean;\n businesses: string[];\n dependsOn: 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'];\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}\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 = 2;\n\nexport type LockReason = 'PLAN' | 'SITE';\n\nexport interface CatalogPermission {\n code: string;\n locked: boolean;\n lockReason: LockReason | null;\n unlockPlans: string[];\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 permissions: CatalogPermission[];\n}\n\nexport type RoleItem = SnapshotRoleTemplate;\n","import { buildDependsMap, cascadeLocked, prereqClosure } from './permission-deps';\nimport type {\n ScopeType,\n SiteType,\n SiteFeatureLocks,\n CatalogPermission,\n FeatureCatalogEntry,\n LockReason,\n PlatformBucket,\n PlatformCodes,\n RoleItem,\n SnapshotFeature,\n SnapshotPlan,\n VersionSnapshot,\n} from './types';\nimport { 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\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): FeatureCatalogEntry[] {\n if (!businessCode) return [];\n const business = snapshot.businesses?.[businessCode];\n if (!business) return [];\n const plan = planCode ? business.plans?.[planCode] : undefined;\n const plans = business.plans ?? {};\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 !!(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 const permissions = buildPermissions(feature, businessCode, membership, siteLocks, plans, bucket);\n // Feature-level lock is EXPLICIT: plan must include the feature on this bucket and the site must not null-lock the platform\n const memberOnBucket = membership?.[bucket] !== undefined;\n const sitePlatformLocked = siteLocks?.[feature.code]?.[bucket] === null;\n const locked = !memberOnBucket || sitePlatformLocked;\n const lockReason: LockReason | null = !memberOnBucket ? 'PLAN' : sitePlatformLocked ? 'SITE' : 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 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 return !!entry && (entry.web !== undefined || entry.mobile !== undefined);\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 (bucket-scoped)\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): CatalogPermission[] {\n const planUnlocked = new Set(planMembership?.[bucket] ?? []);\n const lockEntry = siteLocks?.[feature.code];\n\n const perms = (feature.permissions ?? []).filter((p) => p.isGlobal || p.businesses.includes(businessCode));\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 const locked = lockedSet.has(p.code);\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 planReason = closure.some((c) => directlyPlanLocked.has(c));\n const siteReason = closure.some((c) => directlySiteLocked.has(c));\n // Plan is the ceiling, so a plan-lock anywhere in the closure wins over a site-lock when reporting the reason\n const lockReason: LockReason | null = !locked ? null : planReason ? 'PLAN' : siteReason ? 'SITE' : null;\n const unlockPlans =\n locked && lockReason === 'PLAN' ? plansUnlockingClosure(plans, feature.code, closure, bucket) : [];\n return { code: p.code, locked, lockReason, unlockPlans };\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 { featureAppliesAtNode, isSiteLockedOnPlatform, isPlanMember } from './catalog.builder';\nimport {\n type ScopeType,\n type SiteFeatureLocks,\n type SiteType,\n PLATFORMS,\n type PlatformBucket,\n type SnapshotPlan,\n snapshotFeatureKey,\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}\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 permissions: SiteMatrixPermission[];\n}\n\nexport interface SiteMatrixApp {\n code: string;\n name: string;\n icon: string | null;\n unlockedCount: number;\n totalCount: number;\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 let unlockedCount = 0;\n let totalCount = 0;\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 const platforms = PLATFORMS.filter((p) => !!feature.microfrontends?.[p]);\n if (platforms.length === 0) continue;\n\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 if (!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 totalCount += 1;\n if (inPlan) unlockedCount += 1;\n return { inPlan, selected, availableIn };\n };\n return {\n code: p.code,\n label: p.label,\n dependsOn: p.dependsOn ?? [],\n web: cell('web'),\n mobile: cell('mobile'),\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 permissions,\n });\n }\n\n if (features.length === 0) continue;\n apps.push({ code: app.code, name: app.name, icon: app.icon ?? null, unlockedCount, totalCount, 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","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 if (composed.web === undefined && composed.mobile === 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 SiteFeatureLocks,\n FeatureUnlocks,\n LockReason,\n PlatformBucket,\n ScopeType,\n SiteType,\n VersionSnapshot,\n} from './types';\nimport { snapshotFeatureKey } from './types';\n\nexport type ClientPlatform = 'web' | 'ios' | 'android';\n\nexport interface LockedPermission {\n code: string;\n reason: LockReason | null;\n unlockPlans: string[];\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 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}\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, roleFeatures, platform, siteType, scope } = params;\n\n // Plan unlocks, BU locks, and role grants are stored per platform; resolve only the requesting surface's bucket (web → 'web'; ios/android → 'mobile')\n const bucket: PlatformBucket = platform === 'web' ? 'web' : 'mobile';\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(snapshot, businessCode, planCode, siteLocks, bucket, siteType, scope);\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 const route = pickRouteForPlatform(catalogEntry, platform);\n // Feature isn't published to this platform — omit so client doesn't see an unloadable tile.\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 const grantedPerms = [...filterGrantedByDeps(permsSet, featureDeps)];\n const lockedPermissions: LockedPermission[] = grantedPerms\n .map((c) => permByCode.get(c))\n .filter((p): p is NonNullable<typeof p> => !!p?.locked)\n .map((p) => ({ code: p.code, reason: p.lockReason ?? null, unlockPlans: p.unlockPlans ?? [] }));\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 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 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"],"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","SITE_TYPES","snapshotFeatureKey","code","scope","SNAPSHOT_SCHEMA_VERSION","featureAppliesAtNode","applicableSiteTypes","siteType","includes","findFeatureByCode","snapshot","code","feature","Object","values","features","undefined","buildSiteCatalog","businessCode","planCode","siteLocks","bucket","scope","business","businesses","plan","plans","catalog","sortedApps","apps","sort","a","b","name","localeCompare","app","businessAppFeatures","filter","ref","map","snapshotFeatureKey","f","microfrontends","web","mobile","length","membership","unlockedPermissions","permissions","buildPermissions","memberOnBucket","sitePlatformLocked","locked","lockReason","unlockPlans","plansIncludingFeature","push","lucideIcon","sfSymbol","materialSymbol","remoteEntry","exposedModule","routePrefix","remoteEntryAndroid","remoteEntryIos","appCode","appName","appIcon","icon","appSortOrder","sortOrder","isPlanMember","entry","isSiteLockedOnPlatform","platform","locks","planMembership","planUnlocked","Set","lockEntry","perms","p","isGlobal","deps","buildDependsMap","codes","directlyPlanLocked","directlySiteLocked","has","add","directlyLocked","lockedSet","cascadeLocked","closure","prereqClosure","planReason","some","c","siteReason","plansUnlockingClosure","featureCode","result","entries","unlocked","every","buildSiteRoles","roleTemplates","buildSiteMatrix","snapshot","businessCode","planCode","siteLocks","siteType","buildMatrix","buildPlanMatrix","allScopes","business","businesses","undefined","plans","plan","planMeta","code","name","locks","apps","app","unlockedCount","totalCount","features","ref","scope","feature","snapshotFeatureKey","featureAppliesAtNode","applicableSiteTypes","platforms","PLATFORMS","filter","p","microfrontends","length","membership","unlockedPermissions","featureInPlan","isPlanMember","siteEntry","permissions","isGlobal","includes","map","cell","plat","planCodes","inPlan","selected","isSiteLockedOnPlatform","availableIn","plansUnlockingPerm","label","dependsOn","web","mobile","push","icon","lucideIcon","plansIncludingFeature","sort","a","b","localeCompare","featureCode","permCode","platform","excludeCode","names","Object","entries","unionBucket","base","add","undefined","Set","composeRoleGrants","params","baseFeatures","additions","revoked","result","featureCodes","Object","keys","code","revokes","composed","bucket","PLATFORMS","merged","revoke","filter","c","includes","web","mobile","resolveUserFeatures","params","snapshot","businessCode","planCode","siteLocks","roleFeatures","platform","siteType","scope","bucket","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","pickRouteForPlatform","featureDeps","buildDependsMap","permissions","permByCode","p","grantedPerms","filterGrantedByDeps","lockedPermissions","c","filter","locked","reason","lockReason","unlockPlans","upsell","group","length","lucideIcon","sfSymbol","materialSymbol","appCode","appName","appIcon","appSortOrder","sort","a","b","localeCompare","entry","mobile","remoteEntry","remoteEntryIos","remoteEntryAndroid","exposedModule","routePrefix","web"]}