@vritti/api-sdk 0.4.1 → 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.
@@ -43,7 +43,6 @@ __export(catalog_resolver_exports, {
43
43
  isApiBucket: () => isApiBucket,
44
44
  isPlanMember: () => isPlanMember,
45
45
  isSiteLockedOnPlatform: () => isSiteLockedOnPlatform,
46
- normalizeApiBuckets: () => normalizeApiBuckets,
47
46
  pickRouteForPlatform: () => pickRouteForPlatform,
48
47
  prereqClosure: () => prereqClosure,
49
48
  resolveUserFeatures: () => resolveUserFeatures,
@@ -165,78 +164,40 @@ function snapshotFeatureKey(code, scope) {
165
164
  return `${scope}.${code}`;
166
165
  }
167
166
  __name(snapshotFeatureKey, "snapshotFeatureKey");
168
- var SNAPSHOT_SCHEMA_VERSION = 4;
167
+ var SNAPSHOT_SCHEMA_VERSION = 6;
169
168
 
170
169
  // src/catalog-resolver/catalog.builder.ts
171
- function normalizeApiBuckets(doc) {
172
- if (!doc) return doc;
173
- let changed = false;
174
- const next = {};
175
- for (const [code, entry] of Object.entries(doc)) {
176
- const legacy = entry.app;
177
- if (legacy === void 0 || entry.graphql !== void 0 && entry.http !== void 0) {
178
- next[code] = entry;
179
- continue;
180
- }
181
- changed = true;
182
- next[code] = {
183
- ...entry,
184
- ...entry.graphql === void 0 ? {
185
- graphql: legacy
186
- } : {},
187
- ...entry.http === void 0 ? {
188
- http: legacy
189
- } : {}
190
- };
191
- }
192
- return changed ? next : doc;
193
- }
194
- __name(normalizeApiBuckets, "normalizeApiBuckets");
195
- function normalizePlans(plans) {
196
- let changed = false;
197
- const next = {};
198
- for (const [code, plan] of Object.entries(plans)) {
199
- const unlockedPermissions = normalizeApiBuckets(plan.unlockedPermissions) ?? {};
200
- if (unlockedPermissions !== plan.unlockedPermissions) changed = true;
201
- next[code] = unlockedPermissions === plan.unlockedPermissions ? plan : {
202
- ...plan,
203
- unlockedPermissions
204
- };
205
- }
206
- return changed ? next : plans;
207
- }
208
- __name(normalizePlans, "normalizePlans");
209
170
  function featureAppliesAtNode(applicableSiteTypes, siteType) {
210
171
  return applicableSiteTypes.includes(siteType);
211
172
  }
212
173
  __name(featureAppliesAtNode, "featureAppliesAtNode");
213
174
  function findFeatureByCode(snapshot, code) {
214
- for (const feature of Object.values(snapshot.features ?? {})) {
215
- if (feature?.code === code) return feature;
175
+ for (const feature of Object.values(snapshot.features)) {
176
+ if (feature.code === code) return feature;
216
177
  }
217
178
  return void 0;
218
179
  }
219
180
  __name(findFeatureByCode, "findFeatureByCode");
220
181
  function buildSiteCatalog(snapshot, businessCode, planCode, siteLocks, bucket, siteType, scope, availableServices = []) {
221
182
  if (!businessCode) return [];
222
- const business = snapshot.businesses?.[businessCode];
183
+ const business = snapshot.businesses[businessCode];
223
184
  if (!business) return [];
224
- const plans = normalizePlans(business.plans ?? {});
185
+ const plans = business.plans;
225
186
  const plan = planCode ? plans[planCode] : void 0;
226
- const locks = normalizeApiBuckets(siteLocks);
187
+ const locks = siteLocks;
227
188
  const catalog = [];
228
189
  const sortedApps = [
229
- ...business.apps
190
+ ...snapshot.apps
230
191
  ].sort((a, b) => a.name.localeCompare(b.name));
231
192
  for (const app of sortedApps) {
232
- 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.
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.
233
194
  // An API bucket renders nothing — there a feature is admitted by the surfaces it declares
234
195
  // instead, so a GRAPHQL credential never resolves an HTTP-only feature. A surface-excluded
235
196
  // feature vanishes from the catalog entirely, which is what makes resolution fail closed.
236
197
  (isApiBucket(bucket) ? surfaceAllows(f.apiSurfaces, SURFACE_BY_BUCKET[bucket]) : !!(f.microfrontends?.web || f.microfrontends?.mobile)) && (siteType === void 0 || featureAppliesAtNode(f.applicableSiteTypes, siteType)));
237
198
  if (businessAppFeatures.length === 0) continue;
238
199
  for (const feature of businessAppFeatures) {
239
- const membership = plan?.unlockedPermissions?.[feature.code];
200
+ const membership = plan?.unlockedPermissions[feature.code];
240
201
  const web = feature.microfrontends?.web;
241
202
  const mobile = feature.microfrontends?.mobile;
242
203
  const memberOnBucket = membership?.[bucket] !== void 0;
@@ -280,11 +241,11 @@ function buildSiteCatalog(snapshot, businessCode, planCode, siteLocks, bucket, s
280
241
  __name(buildSiteCatalog, "buildSiteCatalog");
281
242
  function isPlanMember(entry) {
282
243
  if (!entry) return false;
283
- return PLATFORMS.some((platform) => entry[platform] !== void 0) || entry.app !== void 0;
244
+ return PLATFORMS.some((platform) => entry[platform] !== void 0);
284
245
  }
285
246
  __name(isPlanMember, "isPlanMember");
286
247
  function surfaceAllows(surfaces, surface) {
287
- return surface === void 0 || surfaces === void 0 || surfaces.includes(surface);
248
+ return surface === void 0 || surfaces.includes(surface);
288
249
  }
289
250
  __name(surfaceAllows, "surfaceAllows");
290
251
  function resolveLockReason(planLocked, siteLocked, missingServices) {
@@ -295,7 +256,7 @@ function resolveLockReason(planLocked, siteLocked, missingServices) {
295
256
  }
296
257
  __name(resolveLockReason, "resolveLockReason");
297
258
  function unmetServices(feature, availableServices) {
298
- return (feature.requiredServices ?? []).filter((service) => !availableServices.includes(service));
259
+ return feature.requiredServices.filter((service) => !availableServices.includes(service));
299
260
  }
300
261
  __name(unmetServices, "unmetServices");
301
262
  function isSiteLockedOnPlatform(entry, platform, code) {
@@ -306,7 +267,7 @@ __name(isSiteLockedOnPlatform, "isSiteLockedOnPlatform");
306
267
  function buildPermissions(feature, businessCode, planMembership, siteLocks, plans, bucket, missingServices = []) {
307
268
  const planUnlocked = new Set(planMembership?.[bucket] ?? []);
308
269
  const lockEntry = siteLocks?.[feature.code];
309
- const perms = (feature.permissions ?? []).filter((p) => p.isGlobal || p.businesses.includes(businessCode));
270
+ const perms = feature.permissions.filter((p) => p.isGlobal || p.businesses.includes(businessCode)).filter((p) => p.platforms.includes(bucket));
310
271
  const deps = buildDependsMap(perms);
311
272
  const codes = perms.map((p) => p.code);
312
273
  const directlyPlanLocked = /* @__PURE__ */ new Set();
@@ -344,7 +305,7 @@ __name(buildPermissions, "buildPermissions");
344
305
  function plansUnlockingClosure(plans, featureCode, closure, bucket) {
345
306
  const result = [];
346
307
  for (const [code, plan] of Object.entries(plans)) {
347
- const unlocked = plan.unlockedPermissions?.[featureCode]?.[bucket];
308
+ const unlocked = plan.unlockedPermissions[featureCode]?.[bucket];
348
309
  if (unlocked && closure.every((c) => unlocked.includes(c))) result.push(code);
349
310
  }
350
311
  return result;
@@ -353,16 +314,16 @@ __name(plansUnlockingClosure, "plansUnlockingClosure");
353
314
  function plansIncludingFeature(plans, featureCode, bucket) {
354
315
  const result = [];
355
316
  for (const [code, plan] of Object.entries(plans)) {
356
- if (plan.unlockedPermissions?.[featureCode]?.[bucket] !== void 0) result.push(code);
317
+ if (plan.unlockedPermissions[featureCode]?.[bucket] !== void 0) result.push(code);
357
318
  }
358
319
  return result;
359
320
  }
360
321
  __name(plansIncludingFeature, "plansIncludingFeature");
361
322
  function buildSiteRoles(snapshot, businessCode) {
362
323
  if (!businessCode) return [];
363
- const business = snapshot.businesses?.[businessCode];
324
+ const business = snapshot.businesses[businessCode];
364
325
  if (!business) return [];
365
- return Object.values(business.roleTemplates ?? {});
326
+ return Object.values(business.roleTemplates);
366
327
  }
367
328
  __name(buildSiteRoles, "buildSiteRoles");
368
329
 
@@ -382,11 +343,11 @@ function composeRoleGrants(params) {
382
343
  const result = {};
383
344
  const featureCodes = /* @__PURE__ */ new Set([
384
345
  ...Object.keys(baseFeatures ?? {}),
385
- ...Object.keys(additions ?? {})
346
+ ...Object.keys(additions)
386
347
  ]);
387
348
  for (const code of featureCodes) {
388
349
  const base = baseFeatures?.[code] ?? {};
389
- const add = additions?.[code] ?? {};
350
+ const add = additions[code] ?? {};
390
351
  const revokes = revoked?.[code];
391
352
  const composed = {};
392
353
  for (const bucket of PLATFORMS) {
@@ -419,32 +380,26 @@ var EMPTY_ROUTE = {
419
380
  function resolveUserFeatures(params) {
420
381
  const { snapshot, businessCode, planCode, siteLocks, platform, siteType, scope, availableServices } = params;
421
382
  const bucket = BUCKET_BY_CLIENT[platform];
422
- const roleFeatures = normalizeApiBuckets(params.roleFeatures) ?? {};
423
- const featureByCode = /* @__PURE__ */ __name((code) => scope ? snapshot.features?.[snapshotFeatureKey(code, scope)] : findFeatureByCode(snapshot, code), "featureByCode");
383
+ const roleFeatures = params.roleFeatures;
384
+ const featureByCode = /* @__PURE__ */ __name((code) => scope ? snapshot.features[snapshotFeatureKey(code, scope)] : findFeatureByCode(snapshot, code), "featureByCode");
424
385
  const catalog = buildSiteCatalog(snapshot, businessCode, planCode, siteLocks, bucket, siteType, scope, availableServices);
425
386
  const catalogMap = new Map(catalog.map((f) => [
426
387
  f.code,
427
388
  f
428
389
  ]));
429
- const businessPlans = Object.fromEntries(Object.entries(snapshot.businesses[businessCode]?.plans ?? {}).map(([code, plan]) => [
430
- code,
431
- {
432
- ...plan,
433
- unlockedPermissions: normalizeApiBuckets(plan.unlockedPermissions) ?? {}
434
- }
435
- ]));
390
+ const businessPlans = snapshot.businesses[businessCode]?.plans ?? {};
436
391
  const currentUnlockedCodes = /* @__PURE__ */ new Set();
437
392
  if (planCode && businessPlans[planCode]) {
438
- for (const [featureCode, platforms] of Object.entries(businessPlans[planCode].unlockedPermissions ?? {})) {
439
- if (platforms?.[bucket] !== void 0) currentUnlockedCodes.add(featureCode);
393
+ for (const [featureCode, platforms] of Object.entries(businessPlans[planCode].unlockedPermissions)) {
394
+ if (platforms[bucket] !== void 0) currentUnlockedCodes.add(featureCode);
440
395
  }
441
396
  }
442
397
  const planAdds = /* @__PURE__ */ new Map();
443
398
  for (const [planKey, plan] of Object.entries(businessPlans)) {
444
399
  if (planKey === planCode) continue;
445
400
  const adds = [];
446
- for (const [featureCode, platforms] of Object.entries(plan.unlockedPermissions ?? {})) {
447
- if (platforms?.[bucket] === void 0 || currentUnlockedCodes.has(featureCode)) continue;
401
+ for (const [featureCode, platforms] of Object.entries(plan.unlockedPermissions)) {
402
+ if (platforms[bucket] === void 0 || currentUnlockedCodes.has(featureCode)) continue;
448
403
  const name = featureByCode(featureCode)?.name;
449
404
  if (name) adds.push({
450
405
  code: featureCode,
@@ -454,8 +409,8 @@ function resolveUserFeatures(params) {
454
409
  planAdds.set(planKey, adds);
455
410
  }
456
411
  const grantedFeatures = /* @__PURE__ */ new Map();
457
- for (const [code, grant] of Object.entries(roleFeatures ?? {})) {
458
- const granted = grant?.[bucket];
412
+ for (const [code, grant] of Object.entries(roleFeatures)) {
413
+ const granted = grant[bucket];
459
414
  if (granted === void 0) continue;
460
415
  if (!grantedFeatures.has(code)) grantedFeatures.set(code, /* @__PURE__ */ new Set());
461
416
  for (const perm of granted) grantedFeatures.get(code)?.add(perm);
@@ -467,20 +422,20 @@ function resolveUserFeatures(params) {
467
422
  const route = isApiBucket(bucket) ? EMPTY_ROUTE : pickRouteForPlatform(catalogEntry, platform);
468
423
  if (!route) continue;
469
424
  const featureDeps = buildDependsMap(featureByCode(code)?.permissions ?? []);
470
- const permByCode = new Map((catalogEntry.permissions ?? []).map((p) => [
425
+ const permByCode = new Map(catalogEntry.permissions.map((p) => [
471
426
  p.code,
472
427
  p
473
428
  ]));
474
429
  const grantedPerms = [
475
430
  ...filterGrantedByDeps(permsSet, featureDeps)
476
- ];
431
+ ].filter((c) => permByCode.has(c));
477
432
  const lockedPermissions = grantedPerms.map((c) => permByCode.get(c)).filter((p) => !!p?.locked).map((p) => ({
478
433
  code: p.code,
479
434
  reason: p.lockReason ?? null,
480
- unlockPlans: p.unlockPlans ?? [],
481
- missingServices: p.missingServices ?? []
435
+ unlockPlans: p.unlockPlans,
436
+ missingServices: p.missingServices
482
437
  }));
483
- const upsell = catalogEntry.locked && catalogEntry.lockReason === "PLAN" ? (catalogEntry.unlockPlans ?? []).map((plan) => ({
438
+ const upsell = catalogEntry.locked && catalogEntry.lockReason === "PLAN" ? catalogEntry.unlockPlans.map((plan) => ({
484
439
  plan,
485
440
  features: (planAdds.get(plan) ?? []).filter((f) => f.code !== code).map((f) => f.name)
486
441
  })).filter((group) => group.features.length > 0) : [];
@@ -493,8 +448,8 @@ function resolveUserFeatures(params) {
493
448
  permissions: grantedPerms,
494
449
  locked: catalogEntry.locked ?? false,
495
450
  lockReason: catalogEntry.lockReason ?? null,
496
- unlockPlans: catalogEntry.unlockPlans ?? [],
497
- missingServices: catalogEntry.missingServices ?? [],
451
+ unlockPlans: catalogEntry.unlockPlans,
452
+ missingServices: catalogEntry.missingServices,
498
453
  lockedPermissions,
499
454
  upsell,
500
455
  route,
@@ -537,53 +492,66 @@ function buildPlanMatrix(snapshot, businessCode, planCode, siteLocks) {
537
492
  }
538
493
  __name(buildPlanMatrix, "buildPlanMatrix");
539
494
  function buildMatrix(snapshot, businessCode, planCode, siteLocks, allScopes, siteType) {
540
- const business = businessCode ? snapshot.businesses?.[businessCode] : void 0;
541
- const plans = Object.fromEntries(Object.entries(business?.plans ?? {}).map(([code, p]) => [
542
- code,
543
- {
544
- ...p,
545
- unlockedPermissions: normalizeApiBuckets(p.unlockedPermissions) ?? {}
546
- }
547
- ]));
495
+ const business = businessCode ? snapshot.businesses[businessCode] : void 0;
496
+ const plans = business?.plans ?? {};
548
497
  const plan = planCode ? plans[planCode] : void 0;
549
498
  const planMeta = {
550
499
  code: planCode ?? "",
551
500
  name: plan?.name ?? planCode ?? ""
552
501
  };
553
- const normalizedLocks = normalizeApiBuckets(siteLocks);
554
- const locks = normalizedLocks ?? {};
502
+ const locks = siteLocks ?? {};
555
503
  if (!business || !plan) return {
556
504
  plan: planMeta,
557
505
  apps: [],
558
506
  locks
559
507
  };
560
508
  const apps = [];
561
- for (const app of business.apps) {
562
- let unlockedCount = 0;
563
- let totalCount = 0;
509
+ for (const app of snapshot.apps) {
510
+ const counts = {
511
+ web: {
512
+ unlocked: 0,
513
+ total: 0
514
+ },
515
+ mobile: {
516
+ unlocked: 0,
517
+ total: 0
518
+ },
519
+ graphql: {
520
+ unlocked: 0,
521
+ total: 0
522
+ },
523
+ http: {
524
+ unlocked: 0,
525
+ total: 0
526
+ }
527
+ };
564
528
  const features = [];
565
529
  for (const ref of app.features) {
566
530
  if (!allScopes && ref.scope !== "SITE") continue;
567
531
  const code = ref.code;
568
- const feature = snapshot.features?.[snapshotFeatureKey(code, ref.scope)];
532
+ const feature = snapshot.features[snapshotFeatureKey(code, ref.scope)];
569
533
  if (!feature) continue;
570
534
  if (siteType !== void 0 && !featureAppliesAtNode(feature.applicableSiteTypes, siteType)) continue;
571
535
  const platforms = [
572
536
  ...UI_PLATFORMS.filter((p) => !!feature.microfrontends?.[p]),
573
- ...API_BUCKETS.filter((b) => feature.apiSurfaces === void 0 || feature.apiSurfaces.includes(SURFACE_BY_BUCKET[b]))
537
+ ...API_BUCKETS.filter((b) => feature.apiSurfaces.includes(SURFACE_BY_BUCKET[b]))
574
538
  ];
575
- const membership = plan.unlockedPermissions?.[code];
539
+ const groupByCode = new Map(feature.permissionGroups.map((g) => [
540
+ g.code,
541
+ g
542
+ ]));
543
+ const membership = plan.unlockedPermissions[code];
576
544
  const featureInPlan = isPlanMember(membership);
577
- const siteEntry = normalizedLocks?.[code];
578
- const permissions = (feature.permissions ?? []).filter((p) => p.isGlobal || p.businesses.includes(businessCode ?? "")).map((p) => {
545
+ const siteEntry = siteLocks?.[code];
546
+ const permissions = feature.permissions.filter((p) => p.isGlobal || p.businesses.includes(businessCode ?? "")).map((p) => {
579
547
  const cell = /* @__PURE__ */ __name((plat) => {
580
- if (!platforms.includes(plat)) return null;
548
+ if (!platforms.includes(plat) || !p.platforms.includes(plat)) return null;
581
549
  const planCodes = membership?.[plat];
582
550
  const inPlan = featureInPlan && planCodes !== void 0 && planCodes.includes(p.code);
583
551
  const selected = inPlan && !isSiteLockedOnPlatform(siteEntry, plat, p.code);
584
552
  const availableIn = inPlan ? [] : plansUnlockingPerm(plans, code, p.code, plat, planCode);
585
- totalCount += 1;
586
- if (inPlan) unlockedCount += 1;
553
+ counts[plat].total += 1;
554
+ if (inPlan) counts[plat].unlocked += 1;
587
555
  return {
588
556
  inPlan,
589
557
  selected,
@@ -593,7 +561,8 @@ function buildMatrix(snapshot, businessCode, planCode, siteLocks, allScopes, sit
593
561
  return {
594
562
  code: p.code,
595
563
  label: p.label,
596
- dependsOn: p.dependsOn ?? [],
564
+ dependsOn: p.dependsOn,
565
+ group: p.group ? groupByCode.get(p.group) : void 0,
597
566
  web: cell("web"),
598
567
  mobile: cell("mobile"),
599
568
  graphql: cell("graphql"),
@@ -618,8 +587,7 @@ function buildMatrix(snapshot, businessCode, planCode, siteLocks, allScopes, sit
618
587
  code: app.code,
619
588
  name: app.name,
620
589
  icon: app.icon ?? null,
621
- unlockedCount,
622
- totalCount,
590
+ counts,
623
591
  features
624
592
  });
625
593
  }
@@ -635,7 +603,7 @@ function plansUnlockingPerm(plans, featureCode, permCode, platform, excludeCode)
635
603
  const names = [];
636
604
  for (const [code, p] of Object.entries(plans)) {
637
605
  if (code === excludeCode) continue;
638
- if ((p.unlockedPermissions?.[featureCode]?.[platform] ?? []).includes(permCode)) names.push(p.name);
606
+ if ((p.unlockedPermissions[featureCode]?.[platform] ?? []).includes(permCode)) names.push(p.name);
639
607
  }
640
608
  return names;
641
609
  }
@@ -644,7 +612,7 @@ function plansIncludingFeature2(plans, featureCode, excludeCode) {
644
612
  const names = [];
645
613
  for (const [code, p] of Object.entries(plans)) {
646
614
  if (code === excludeCode) continue;
647
- if (isPlanMember(p.unlockedPermissions?.[featureCode])) names.push(p.name);
615
+ if (isPlanMember(p.unlockedPermissions[featureCode])) names.push(p.name);
648
616
  }
649
617
  return names;
650
618
  }
@@ -673,7 +641,6 @@ __name(plansIncludingFeature2, "plansIncludingFeature");
673
641
  isApiBucket,
674
642
  isPlanMember,
675
643
  isSiteLockedOnPlatform,
676
- normalizeApiBuckets,
677
644
  pickRouteForPlatform,
678
645
  prereqClosure,
679
646
  resolveUserFeatures,