@fayz-ai/core 0.44.0 → 0.48.0

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 (61) hide show
  1. package/dist/access/engine.d.ts +4 -3
  2. package/dist/access/engine.d.ts.map +1 -1
  3. package/dist/access/index.d.ts +2 -0
  4. package/dist/access/index.d.ts.map +1 -1
  5. package/dist/access/index.js +27 -3
  6. package/dist/access/index.js.map +1 -1
  7. package/dist/access/module-access.d.ts +31 -0
  8. package/dist/access/module-access.d.ts.map +1 -0
  9. package/dist/app/app-config.d.ts +15 -0
  10. package/dist/app/app-config.d.ts.map +1 -1
  11. package/dist/{chunk-5AN766JZ.js → chunk-5NVZ2NOO.js} +30 -7
  12. package/dist/chunk-5NVZ2NOO.js.map +1 -0
  13. package/dist/{chunk-QTEN5EL7.js → chunk-6ODXG6AJ.js} +13 -3
  14. package/dist/chunk-6ODXG6AJ.js.map +1 -0
  15. package/dist/{chunk-ZNXUGLL3.js → chunk-JHKAZA4T.js} +3 -3
  16. package/dist/{chunk-ZNXUGLL3.js.map → chunk-JHKAZA4T.js.map} +1 -1
  17. package/dist/{chunk-TDZO4LUS.js → chunk-KALDZT7C.js} +3 -3
  18. package/dist/{chunk-TDZO4LUS.js.map → chunk-KALDZT7C.js.map} +1 -1
  19. package/dist/{chunk-NI7VLCFI.js → chunk-R7LJOOJI.js} +4 -4
  20. package/dist/{chunk-NI7VLCFI.js.map → chunk-R7LJOOJI.js.map} +1 -1
  21. package/dist/{chunk-45BR57HO.js → chunk-UHLFFV6S.js} +4 -4
  22. package/dist/{chunk-45BR57HO.js.map → chunk-UHLFFV6S.js.map} +1 -1
  23. package/dist/{chunk-GCGFDVEZ.js → chunk-Y5QS5KBU.js} +4 -4
  24. package/dist/{chunk-GCGFDVEZ.js.map → chunk-Y5QS5KBU.js.map} +1 -1
  25. package/dist/connector-runtime/index.d.ts +2 -1
  26. package/dist/connector-runtime/index.d.ts.map +1 -1
  27. package/dist/data/index.js +4 -4
  28. package/dist/i18n/index.js +1 -1
  29. package/dist/i18n/shell-translations.en.d.ts.map +1 -1
  30. package/dist/i18n/shell-translations.pt-br.d.ts.map +1 -1
  31. package/dist/index.d.ts +17 -9
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +321 -44
  34. package/dist/index.js.map +1 -1
  35. package/dist/integrations/index.js +4 -4
  36. package/dist/lib/address.d.ts +14 -9
  37. package/dist/lib/address.d.ts.map +1 -1
  38. package/dist/lib/countries.d.ts +74 -0
  39. package/dist/lib/countries.d.ts.map +1 -0
  40. package/dist/plugin/index.js +3 -3
  41. package/dist/plugin/runtime.d.ts +8 -0
  42. package/dist/plugin/runtime.d.ts.map +1 -1
  43. package/dist/{shell-translations.pt-br-CPAZDDTQ.js → shell-translations.pt-br-DA5IHWK5.js} +12 -2
  44. package/dist/shell-translations.pt-br-DA5IHWK5.js.map +1 -0
  45. package/dist/testing/index.js +5 -5
  46. package/dist/types/billing.d.ts +13 -0
  47. package/dist/types/billing.d.ts.map +1 -1
  48. package/dist/types/crud.d.ts +20 -0
  49. package/dist/types/crud.d.ts.map +1 -1
  50. package/dist/types/index.d.ts +2 -2
  51. package/dist/types/index.d.ts.map +1 -1
  52. package/dist/types/plugins.d.ts +58 -0
  53. package/dist/types/plugins.d.ts.map +1 -1
  54. package/package.json +2 -2
  55. package/dist/chunk-5AN766JZ.js.map +0 -1
  56. package/dist/chunk-QTEN5EL7.js.map +0 -1
  57. package/dist/messaging/index.d.ts +0 -6
  58. package/dist/messaging/index.d.ts.map +0 -1
  59. package/dist/search/index.d.ts +0 -5
  60. package/dist/search/index.d.ts.map +0 -1
  61. package/dist/shell-translations.pt-br-CPAZDDTQ.js.map +0 -1
@@ -73,9 +73,10 @@ export declare function isEntitledByPlan(plan: Plan | null, feature: string): bo
73
73
  */
74
74
  export declare function mergeProfiles(base: PermissionProfile | null, overlay: PermissionProfile | null): PermissionProfile | null;
75
75
  /**
76
- * The profile that applies where the person is standing: their org role, plus
77
- * whatever the unit adds. With no unit — or no binding in it — this is exactly
78
- * the org role, which is why nothing that predates units changes behaviour.
76
+ * The profile that applies where the person is standing: the unit's role when
77
+ * the person has one there, otherwise the org role. The unit role REPLACES the
78
+ * org role, matching `app.effective_role_id` — a union here would show actions
79
+ * the database then refuses. The owner stays owner everywhere.
79
80
  */
80
81
  export declare function effectiveProfileFor(subject: AccessSubject, unitId?: string): PermissionProfile | null;
81
82
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/access/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACjG,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAe5C,+EAA+E;AAC/E,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AAElD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,UAAU,CAAA;CACpB;AAED,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,wEAAwE;IACxE,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACjC,0CAA0C;IAC1C,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;IACjB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;IAChD,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAElE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,iBAAiB,GAAG,IAAI,EACjC,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAeT;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iBAAiB,GAAG,IAAI,EACjC,UAAU,EAAE,gBAAgB,GAC3B,OAAO,CAIT;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAE5E;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,iBAAiB,GAAG,IAAI,EAC9B,OAAO,EAAE,iBAAiB,GAAG,IAAI,GAChC,iBAAiB,GAAG,IAAI,CAmB1B;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,aAAa,EACtB,MAAM,CAAC,EAAE,MAAM,GACd,iBAAiB,GAAG,IAAI,CAG1B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,gBAAgB,EACzB,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5B,cAAc,CAShB;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IACxG,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,OAAO,CAAA;CACnB,CAOA"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/access/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACjG,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAe5C,+EAA+E;AAC/E,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AAElD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,UAAU,CAAA;CACpB;AAED,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,wEAAwE;IACxE,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACjC,0CAA0C;IAC1C,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;IACjB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;IAChD,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAElE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,iBAAiB,GAAG,IAAI,EACjC,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAeT;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iBAAiB,GAAG,IAAI,EACjC,UAAU,EAAE,gBAAgB,GAC3B,OAAO,CAIT;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAE5E;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,iBAAiB,GAAG,IAAI,EAC9B,OAAO,EAAE,iBAAiB,GAAG,IAAI,GAChC,iBAAiB,GAAG,IAAI,CAmB1B;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,aAAa,EACtB,MAAM,CAAC,EAAE,MAAM,GACd,iBAAiB,GAAG,IAAI,CAG1B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,gBAAgB,EACzB,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5B,cAAc,CAShB;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IACxG,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,OAAO,CAAA;CACnB,CAOA"}
@@ -3,4 +3,6 @@ export type { DenyReason, AccessDecision, AccessSubject } from './engine';
3
3
  export { UPGRADE_URL, agentDenial } from './denial';
4
4
  export type { AgentDenial, AgentDenialLimit, AgentGuardResult } from './denial';
5
5
  export { mergeLimitDeclarations } from './limits';
6
+ export { resolveModuleAccess, createModuleAccess } from './module-access';
7
+ export type { ModuleDenyReason, ModuleAccessQuery, ModuleAccessDecision, ModuleAccessContext } from './module-access';
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/access/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,0BAA0B,EAC1B,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,aAAa,EACb,mBAAmB,GACpB,MAAM,UAAU,CAAA;AACjB,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAEzE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AACnD,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAE/E,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/access/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,0BAA0B,EAC1B,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,aAAa,EACb,mBAAmB,GACpB,MAAM,UAAU,CAAA;AACjB,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAEzE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AACnD,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAE/E,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAEjD,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACzE,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA"}
@@ -35,8 +35,8 @@ function mergeProfiles(base, overlay) {
35
35
  };
36
36
  }
37
37
  function effectiveProfileFor(subject, unitId) {
38
- if (!unitId) return subject.profile;
39
- return mergeProfiles(subject.profile, subject.unitProfiles?.[unitId] ?? null);
38
+ if (!unitId || subject.profile && isOwnerProfile(subject.profile)) return subject.profile;
39
+ return subject.unitProfiles?.[unitId] ?? subject.profile;
40
40
  }
41
41
  function resolveAccess(subject, feature, action, options) {
42
42
  const profile = effectiveProfileFor(subject, options?.unitId ?? subject.activeUnitId);
@@ -72,6 +72,30 @@ function mergeLimitDeclarations(...layers) {
72
72
  return Array.from(byKey.values());
73
73
  }
74
74
 
75
- export { UPGRADE_URL, agentDenial, effectiveProfileFor, isEntitledByPlan, isOwnerProfile, mergeLimitDeclarations, mergeProfiles, profileHasPermission, profileHasSystemPermission, resolveAccess, resolveLimit };
75
+ // src/access/module-access.ts
76
+ function resolveModuleAccess(q, ctx) {
77
+ const deny = (reason) => ({ allowed: false, reason, plugin: q.plugin, facet: q.facet });
78
+ if (!ctx.isModuleOn(q.plugin, q.facet)) {
79
+ return deny(ctx.entitled && !ctx.entitled(q.plugin, q.facet) ? "not_in_plan" : "module_off");
80
+ }
81
+ if (ctx.unitDisabled.has(q.plugin) || q.facet && ctx.unitDisabled.has(`${q.plugin}.${q.facet}`)) return deny("unit_off");
82
+ if (ctx.entitled && !ctx.entitled(q.plugin, q.facet)) return deny("not_in_plan");
83
+ if (q.permission && ctx.can && !ctx.can(q.permission.feature, q.permission.action)) return deny("no_permission");
84
+ return { allowed: true };
85
+ }
86
+ function createModuleAccess(ctx) {
87
+ const cache = /* @__PURE__ */ new Map();
88
+ return (q) => {
89
+ const key = `${q.plugin}|${q.facet ?? ""}|${q.permission ? `${q.permission.feature}:${q.permission.action}` : ""}`;
90
+ let hit = cache.get(key);
91
+ if (!hit) {
92
+ hit = resolveModuleAccess(q, ctx);
93
+ cache.set(key, hit);
94
+ }
95
+ return hit;
96
+ };
97
+ }
98
+
99
+ export { UPGRADE_URL, agentDenial, createModuleAccess, effectiveProfileFor, isEntitledByPlan, isOwnerProfile, mergeLimitDeclarations, mergeProfiles, profileHasPermission, profileHasSystemPermission, resolveAccess, resolveLimit, resolveModuleAccess };
76
100
  //# sourceMappingURL=index.js.map
77
101
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/access/engine.ts","../../src/access/denial.ts","../../src/access/limits.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { PermissionProfile, PermissionAction, SystemPermission } from '../types/permissions'\nimport type { Plan } from '../types/billing'\n\n// ---------------------------------------------------------------------------\n// The access engine — the ONE implementation of the role × plan × limit\n// decision, shared by every runtime that gates anything:\n//\n// - browser (saas AccessProvider / PermissionsProvider re-export from here)\n// - headless client-plane (saas access/headless.ts)\n// - the Fayz broker (server-side agent authorization imports this package)\n//\n// Hard constraint: this module (and everything under core/src/access) must stay\n// free of React, zustand and supabase — pure functions over plain data — so the\n// exact same code runs in Node/edge. Guarded by scripts/check-access-purity.mjs.\n// ---------------------------------------------------------------------------\n\n/** Why an access request was denied. `limit` only occurs on guarded writes. */\nexport type DenyReason = 'role' | 'plan' | 'limit'\n\nexport interface AccessDecision {\n allowed: boolean\n reason?: DenyReason\n}\n\n/** The minimal subject a decision needs — a projection of any richer session. */\nexport interface AccessSubject {\n /** Effective RBAC profile — the previewed role during impersonation. */\n profile: PermissionProfile | null\n /** The tenant's currently-active plan. */\n plan: Plan | null\n /**\n * Extra role a person has INSIDE a given unit, keyed by unit id.\n *\n * Additive, never subtractive: the org role applies everywhere and a unit role\n * only adds inside its unit. That choice is what makes this shippable — the\n * answer without a unit is a strict SUBSET of the answer with one, so every\n * existing call site keeps its exact meaning and none of them had to change.\n * The alternative (a unit role that overrides) can take access away by\n * accident, and every screen would have to explain which role won.\n */\n unitProfiles?: Record<string, PermissionProfile>\n /** The unit the person is looking at, used when a check does not name one. */\n activeUnitId?: string\n}\n\n/**\n * The org owner has every permission implicitly. Owner is represented by the\n * canonical role id `owner` across adapters (see org/adapters/supabase.ts\n * buildPermissionProfiles, which also grants owner all catalog perms, and the\n * mock adapter's `owner` fallback). We match by id first, falling back to the\n * (possibly localized) name for safety.\n */\nexport function isOwnerProfile(profile: PermissionProfile): boolean {\n return profile.id === 'owner' || profile.name?.toLowerCase() === 'owner'\n}\n\n/**\n * The single role-side permission check. The plan axis composes ON TOP of it in\n * {@link resolveAccess} instead of reimplementing owner-bypass / `manage`\n * semantics.\n *\n * Fails CLOSED on a missing profile. \"Not hydrated yet\" and \"no grant\" are\n * different situations but the same answer — no — because the alternative is a\n * frame where every financial widget paints open to someone who may not be\n * allowed to see it. The genuinely different third case, a runtime with no RBAC\n * at all (an SDK component under a bare host shell), is NOT representable as a\n * null profile: it is the absence of a permissions runtime, and it is decided\n * one level up by `usePermissionOptional` in admin/permissions/context.tsx.\n */\nexport function profileHasPermission(\n profile: PermissionProfile | null,\n feature: string,\n action?: string,\n): boolean {\n if (!profile) return false\n\n // Owner bypass: never lock the owner out, even if the RBAC catalog is empty\n // or still loading. Because permission checks read the *effective* profile,\n // an owner previewing a non-owner role correctly loses this bypass — the\n // preview stays honest.\n if (isOwnerProfile(profile)) return true\n\n const actions = profile.grants[feature]\n if (!actions) return false\n\n if (!action) return actions.length > 0\n\n return actions.includes(action) || actions.includes('manage')\n}\n\n/**\n * The org-management side of a profile: managing team, billing, settings and\n * permissions themselves. Separate axis from `grants` — these gate the settings\n * tabs, not a feature's rows — and it follows the same owner bypass, so the one\n * person who can hand out access is never locked out of doing it. Same\n * fail-closed rule as {@link profileHasPermission} for a missing profile.\n */\nexport function profileHasSystemPermission(\n profile: PermissionProfile | null,\n permission: SystemPermission,\n): boolean {\n if (!profile) return false\n if (isOwnerProfile(profile)) return true\n return profile.systemPermissions?.includes(permission) ?? false\n}\n\n/**\n * Does the current plan entitle `feature`? A plan gates a feature ONLY with an\n * explicit `false`; absent means \"not gated by this plan\" (allowed). This keeps\n * entitlements additive and safe: a feature the plan has never heard of stays\n * open rather than silently locking.\n */\nexport function isEntitledByPlan(plan: Plan | null, feature: string): boolean {\n return plan?.entitlements?.features?.[feature] !== false\n}\n\n/**\n * Union two profiles. Grants merge per feature, system permissions merge, and\n * the result is never smaller than either input — the additive rule, in code.\n *\n * `manage` is not expanded here: {@link profileHasPermission} already treats it\n * as satisfying any action, so expanding it would be a second implementation of\n * the same rule waiting to disagree with the first.\n */\nexport function mergeProfiles(\n base: PermissionProfile | null,\n overlay: PermissionProfile | null,\n): PermissionProfile | null {\n if (!overlay) return base\n if (!base) return overlay\n // An owner already has everything; merging onto it can only produce noise.\n if (isOwnerProfile(base)) return base\n\n const grants: Record<string, PermissionAction[]> = { ...base.grants }\n for (const [feature, actions] of Object.entries(overlay.grants ?? {})) {\n const merged = new Set([...(grants[feature] ?? []), ...actions])\n grants[feature] = [...merged]\n }\n\n const system = new Set([...(base.systemPermissions ?? []), ...(overlay.systemPermissions ?? [])])\n\n return {\n ...base,\n grants,\n systemPermissions: system.size > 0 ? [...system] : undefined,\n }\n}\n\n/**\n * The profile that applies where the person is standing: their org role, plus\n * whatever the unit adds. With no unit — or no binding in it — this is exactly\n * the org role, which is why nothing that predates units changes behaviour.\n */\nexport function effectiveProfileFor(\n subject: AccessSubject,\n unitId?: string,\n): PermissionProfile | null {\n if (!unitId) return subject.profile\n return mergeProfiles(subject.profile, subject.unitProfiles?.[unitId] ?? null)\n}\n\n/**\n * The single access decision: role FIRST, then plan.\n *\n * 1. Role — reuses `profileHasPermission` (owner bypass + `manage` semantics\n * live there, and impersonation composes for free because `profile` is the\n * previewed role). Denied → `{ allowed:false, reason:'role' }`.\n * 2. Plan — even the owner does NOT bypass the plan: upgrading is the tenant's\n * decision, not a role power. Denied → `{ allowed:false, reason:'plan' }`.\n *\n * A subject whose profile has not resolved yet denies with `reason:'role'` —\n * an unknown answer is not permission. Callers that must tell a still-loading\n * denial apart from a final one (to paint a skeleton instead of a lock) ask the\n * permissions runtime, not this decision: see `usePermissions().status`.\n */\nexport function resolveAccess(\n subject: AccessSubject,\n feature: string,\n action?: PermissionAction,\n options?: { unitId?: string },\n): AccessDecision {\n const profile = effectiveProfileFor(subject, options?.unitId ?? subject.activeUnitId)\n if (!profileHasPermission(profile, feature, action)) {\n return { allowed: false, reason: 'role' }\n }\n if (!isEntitledByPlan(subject.plan, feature)) {\n return { allowed: false, reason: 'plan' }\n }\n return { allowed: true }\n}\n\n/**\n * Pure numeric side of a limit: given the plan cap, whether a countable\n * declaration exists, and the current usage, derive the limit math. Unlimited\n * when there's no declaration to count, no cap on the plan, or the cap is `-1`.\n */\nexport function resolveLimit(params: { cap: number | undefined; hasDeclaration: boolean; used: number }): {\n max: number\n used: number\n remaining: number\n atLimit: boolean\n unlimited: boolean\n} {\n const { cap, hasDeclaration, used } = params\n const unlimited = !hasDeclaration || cap === undefined || cap === -1\n const max = unlimited ? Infinity : cap\n const remaining = unlimited ? Infinity : Math.max(0, max - used)\n const atLimit = !unlimited && used >= max\n return { max, used, remaining, atLimit, unlimited }\n}\n","import type { DenyReason } from './engine'\n\n// ---------------------------------------------------------------------------\n// The structured denial an AGENT surface returns instead of failing silently\n// (docs/saas/ENTITLEMENTS.md § \"Superfície de agentes de IA\"). One shape for every\n// executor — FAB client-plane, the Fayz broker, and the pool RPCs (which emit\n// its jsonb mirror in their `denial` field) — so the model can explain the\n// denial and offer the upgrade path conversationally.\n// ---------------------------------------------------------------------------\n\n/** Where a denied tenant goes to upgrade — the conversational UpgradeModal. */\nexport const UPGRADE_URL = '/settings/subscription'\n\nexport interface AgentDenialLimit {\n key: string\n max: number\n used: number\n}\n\nexport interface AgentDenial {\n allowed: false\n reason: DenyReason\n /** Present when `reason === 'limit'`. */\n limit?: AgentDenialLimit\n upgradeUrl: string\n}\n\nexport type AgentGuardResult = { allowed: true } | AgentDenial\n\n/** Build a denial with the canonical upgrade URL attached. */\nexport function agentDenial(reason: DenyReason, limit?: AgentDenialLimit): AgentDenial {\n return { allowed: false, reason, ...(limit ? { limit } : {}), upgradeUrl: UPGRADE_URL }\n}\n","import type { LimitDeclaration } from '../types/entitlements'\n\n/**\n * The 4-layer limit-declaration merge, extracted so the browser AccessProvider\n * and the manifest derivation (`fayz manifest emit`) resolve the EXACT same\n * final set: core built-ins < entity-derived < plugin `declaredLimits` < app\n * overrides. Later layers win by key. Pure: pass the layers in, get the merge\n * out — deriving a declaration FROM an entity stays at the call site.\n */\nexport function mergeLimitDeclarations(\n ...layers: Array<LimitDeclaration[] | undefined>\n): LimitDeclaration[] {\n const byKey = new Map<string, LimitDeclaration>()\n for (const layer of layers) {\n for (const decl of layer ?? []) byKey.set(decl.key, decl)\n }\n return Array.from(byKey.values())\n}\n"]}
1
+ {"version":3,"sources":["../../src/access/engine.ts","../../src/access/denial.ts","../../src/access/limits.ts","../../src/access/module-access.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { PermissionProfile, PermissionAction, SystemPermission } from '../types/permissions'\nimport type { Plan } from '../types/billing'\n\n// ---------------------------------------------------------------------------\n// The access engine — the ONE implementation of the role × plan × limit\n// decision, shared by every runtime that gates anything:\n//\n// - browser (saas AccessProvider / PermissionsProvider re-export from here)\n// - headless client-plane (saas access/headless.ts)\n// - the Fayz broker (server-side agent authorization imports this package)\n//\n// Hard constraint: this module (and everything under core/src/access) must stay\n// free of React, zustand and supabase — pure functions over plain data — so the\n// exact same code runs in Node/edge. Guarded by scripts/check-access-purity.mjs.\n// ---------------------------------------------------------------------------\n\n/** Why an access request was denied. `limit` only occurs on guarded writes. */\nexport type DenyReason = 'role' | 'plan' | 'limit'\n\nexport interface AccessDecision {\n allowed: boolean\n reason?: DenyReason\n}\n\n/** The minimal subject a decision needs — a projection of any richer session. */\nexport interface AccessSubject {\n /** Effective RBAC profile — the previewed role during impersonation. */\n profile: PermissionProfile | null\n /** The tenant's currently-active plan. */\n plan: Plan | null\n /**\n * Extra role a person has INSIDE a given unit, keyed by unit id.\n *\n * Additive, never subtractive: the org role applies everywhere and a unit role\n * only adds inside its unit. That choice is what makes this shippable — the\n * answer without a unit is a strict SUBSET of the answer with one, so every\n * existing call site keeps its exact meaning and none of them had to change.\n * The alternative (a unit role that overrides) can take access away by\n * accident, and every screen would have to explain which role won.\n */\n unitProfiles?: Record<string, PermissionProfile>\n /** The unit the person is looking at, used when a check does not name one. */\n activeUnitId?: string\n}\n\n/**\n * The org owner has every permission implicitly. Owner is represented by the\n * canonical role id `owner` across adapters (see org/adapters/supabase.ts\n * buildPermissionProfiles, which also grants owner all catalog perms, and the\n * mock adapter's `owner` fallback). We match by id first, falling back to the\n * (possibly localized) name for safety.\n */\nexport function isOwnerProfile(profile: PermissionProfile): boolean {\n return profile.id === 'owner' || profile.name?.toLowerCase() === 'owner'\n}\n\n/**\n * The single role-side permission check. The plan axis composes ON TOP of it in\n * {@link resolveAccess} instead of reimplementing owner-bypass / `manage`\n * semantics.\n *\n * Fails CLOSED on a missing profile. \"Not hydrated yet\" and \"no grant\" are\n * different situations but the same answer — no — because the alternative is a\n * frame where every financial widget paints open to someone who may not be\n * allowed to see it. The genuinely different third case, a runtime with no RBAC\n * at all (an SDK component under a bare host shell), is NOT representable as a\n * null profile: it is the absence of a permissions runtime, and it is decided\n * one level up by `usePermissionOptional` in admin/permissions/context.tsx.\n */\nexport function profileHasPermission(\n profile: PermissionProfile | null,\n feature: string,\n action?: string,\n): boolean {\n if (!profile) return false\n\n // Owner bypass: never lock the owner out, even if the RBAC catalog is empty\n // or still loading. Because permission checks read the *effective* profile,\n // an owner previewing a non-owner role correctly loses this bypass — the\n // preview stays honest.\n if (isOwnerProfile(profile)) return true\n\n const actions = profile.grants[feature]\n if (!actions) return false\n\n if (!action) return actions.length > 0\n\n return actions.includes(action) || actions.includes('manage')\n}\n\n/**\n * The org-management side of a profile: managing team, billing, settings and\n * permissions themselves. Separate axis from `grants` — these gate the settings\n * tabs, not a feature's rows — and it follows the same owner bypass, so the one\n * person who can hand out access is never locked out of doing it. Same\n * fail-closed rule as {@link profileHasPermission} for a missing profile.\n */\nexport function profileHasSystemPermission(\n profile: PermissionProfile | null,\n permission: SystemPermission,\n): boolean {\n if (!profile) return false\n if (isOwnerProfile(profile)) return true\n return profile.systemPermissions?.includes(permission) ?? false\n}\n\n/**\n * Does the current plan entitle `feature`? A plan gates a feature ONLY with an\n * explicit `false`; absent means \"not gated by this plan\" (allowed). This keeps\n * entitlements additive and safe: a feature the plan has never heard of stays\n * open rather than silently locking.\n */\nexport function isEntitledByPlan(plan: Plan | null, feature: string): boolean {\n return plan?.entitlements?.features?.[feature] !== false\n}\n\n/**\n * Union two profiles. Grants merge per feature, system permissions merge, and\n * the result is never smaller than either input — the additive rule, in code.\n *\n * `manage` is not expanded here: {@link profileHasPermission} already treats it\n * as satisfying any action, so expanding it would be a second implementation of\n * the same rule waiting to disagree with the first.\n */\nexport function mergeProfiles(\n base: PermissionProfile | null,\n overlay: PermissionProfile | null,\n): PermissionProfile | null {\n if (!overlay) return base\n if (!base) return overlay\n // An owner already has everything; merging onto it can only produce noise.\n if (isOwnerProfile(base)) return base\n\n const grants: Record<string, PermissionAction[]> = { ...base.grants }\n for (const [feature, actions] of Object.entries(overlay.grants ?? {})) {\n const merged = new Set([...(grants[feature] ?? []), ...actions])\n grants[feature] = [...merged]\n }\n\n const system = new Set([...(base.systemPermissions ?? []), ...(overlay.systemPermissions ?? [])])\n\n return {\n ...base,\n grants,\n systemPermissions: system.size > 0 ? [...system] : undefined,\n }\n}\n\n/**\n * The profile that applies where the person is standing: the unit's role when\n * the person has one there, otherwise the org role. The unit role REPLACES the\n * org role, matching `app.effective_role_id` — a union here would show actions\n * the database then refuses. The owner stays owner everywhere.\n */\nexport function effectiveProfileFor(\n subject: AccessSubject,\n unitId?: string,\n): PermissionProfile | null {\n if (!unitId || (subject.profile && isOwnerProfile(subject.profile))) return subject.profile\n return subject.unitProfiles?.[unitId] ?? subject.profile\n}\n\n/**\n * The single access decision: role FIRST, then plan.\n *\n * 1. Role — reuses `profileHasPermission` (owner bypass + `manage` semantics\n * live there, and impersonation composes for free because `profile` is the\n * previewed role). Denied → `{ allowed:false, reason:'role' }`.\n * 2. Plan — even the owner does NOT bypass the plan: upgrading is the tenant's\n * decision, not a role power. Denied → `{ allowed:false, reason:'plan' }`.\n *\n * A subject whose profile has not resolved yet denies with `reason:'role'` —\n * an unknown answer is not permission. Callers that must tell a still-loading\n * denial apart from a final one (to paint a skeleton instead of a lock) ask the\n * permissions runtime, not this decision: see `usePermissions().status`.\n */\nexport function resolveAccess(\n subject: AccessSubject,\n feature: string,\n action?: PermissionAction,\n options?: { unitId?: string },\n): AccessDecision {\n const profile = effectiveProfileFor(subject, options?.unitId ?? subject.activeUnitId)\n if (!profileHasPermission(profile, feature, action)) {\n return { allowed: false, reason: 'role' }\n }\n if (!isEntitledByPlan(subject.plan, feature)) {\n return { allowed: false, reason: 'plan' }\n }\n return { allowed: true }\n}\n\n/**\n * Pure numeric side of a limit: given the plan cap, whether a countable\n * declaration exists, and the current usage, derive the limit math. Unlimited\n * when there's no declaration to count, no cap on the plan, or the cap is `-1`.\n */\nexport function resolveLimit(params: { cap: number | undefined; hasDeclaration: boolean; used: number }): {\n max: number\n used: number\n remaining: number\n atLimit: boolean\n unlimited: boolean\n} {\n const { cap, hasDeclaration, used } = params\n const unlimited = !hasDeclaration || cap === undefined || cap === -1\n const max = unlimited ? Infinity : cap\n const remaining = unlimited ? Infinity : Math.max(0, max - used)\n const atLimit = !unlimited && used >= max\n return { max, used, remaining, atLimit, unlimited }\n}\n","import type { DenyReason } from './engine'\n\n// ---------------------------------------------------------------------------\n// The structured denial an AGENT surface returns instead of failing silently\n// (docs/saas/ENTITLEMENTS.md § \"Superfície de agentes de IA\"). One shape for every\n// executor — FAB client-plane, the Fayz broker, and the pool RPCs (which emit\n// its jsonb mirror in their `denial` field) — so the model can explain the\n// denial and offer the upgrade path conversationally.\n// ---------------------------------------------------------------------------\n\n/** Where a denied tenant goes to upgrade — the conversational UpgradeModal. */\nexport const UPGRADE_URL = '/settings/subscription'\n\nexport interface AgentDenialLimit {\n key: string\n max: number\n used: number\n}\n\nexport interface AgentDenial {\n allowed: false\n reason: DenyReason\n /** Present when `reason === 'limit'`. */\n limit?: AgentDenialLimit\n upgradeUrl: string\n}\n\nexport type AgentGuardResult = { allowed: true } | AgentDenial\n\n/** Build a denial with the canonical upgrade URL attached. */\nexport function agentDenial(reason: DenyReason, limit?: AgentDenialLimit): AgentDenial {\n return { allowed: false, reason, ...(limit ? { limit } : {}), upgradeUrl: UPGRADE_URL }\n}\n","import type { LimitDeclaration } from '../types/entitlements'\n\n/**\n * The 4-layer limit-declaration merge, extracted so the browser AccessProvider\n * and the manifest derivation (`fayz manifest emit`) resolve the EXACT same\n * final set: core built-ins < entity-derived < plugin `declaredLimits` < app\n * overrides. Later layers win by key. Pure: pass the layers in, get the merge\n * out — deriving a declaration FROM an entity stays at the call site.\n */\nexport function mergeLimitDeclarations(\n ...layers: Array<LimitDeclaration[] | undefined>\n): LimitDeclaration[] {\n const byKey = new Map<string, LimitDeclaration>()\n for (const layer of layers) {\n for (const decl of layer ?? []) byKey.set(decl.key, decl)\n }\n return Array.from(byKey.values())\n}\n","// The one answer to \"can this person use this module here, now?\" — the account\n// has it on, the unit they stand in did not switch it off, their plan includes\n// it, and their role allows it. Pure and memoized over the boot snapshot, so a\n// page asks as often as it likes without a request.\n\nexport type ModuleDenyReason = 'not_in_plan' | 'module_off' | 'unit_off' | 'no_permission'\n\nexport interface ModuleAccessQuery {\n plugin: string\n facet?: string\n permission?: { feature: string; action: string }\n}\n\nexport type ModuleAccessDecision =\n | { allowed: true }\n | { allowed: false; reason: ModuleDenyReason; plugin: string; facet?: string }\n\nexport interface ModuleAccessContext {\n /** The module (or facet) is on for the account, as the runtime resolved it. */\n isModuleOn: (plugin: string, facet?: string) => boolean\n /** `plugin` / `plugin.facet` keys the active unit switched off. */\n unitDisabled: ReadonlySet<string>\n /** The plan includes it. Absent: nothing is gated by plan. */\n entitled?: (plugin: string, facet?: string) => boolean\n /** The person's role allows it. Absent: nothing is gated by role. */\n can?: (feature: string, action: string) => boolean\n}\n\nexport function resolveModuleAccess(q: ModuleAccessQuery, ctx: ModuleAccessContext): ModuleAccessDecision {\n const deny = (reason: ModuleDenyReason): ModuleAccessDecision => ({ allowed: false, reason, plugin: q.plugin, facet: q.facet })\n if (!ctx.isModuleOn(q.plugin, q.facet)) {\n // Off because the plan does not reach it reads differently from off by\n // choice: one is \"upgrade\", the other \"turn it on\".\n return deny(ctx.entitled && !ctx.entitled(q.plugin, q.facet) ? 'not_in_plan' : 'module_off')\n }\n if (ctx.unitDisabled.has(q.plugin) || (q.facet && ctx.unitDisabled.has(`${q.plugin}.${q.facet}`))) return deny('unit_off')\n if (ctx.entitled && !ctx.entitled(q.plugin, q.facet)) return deny('not_in_plan')\n if (q.permission && ctx.can && !ctx.can(q.permission.feature, q.permission.action)) return deny('no_permission')\n return { allowed: true }\n}\n\n/** `canUseHere` over one snapshot: every distinct question is answered once. */\nexport function createModuleAccess(ctx: ModuleAccessContext): (q: ModuleAccessQuery) => ModuleAccessDecision {\n const cache = new Map<string, ModuleAccessDecision>()\n return (q) => {\n const key = `${q.plugin}|${q.facet ?? ''}|${q.permission ? `${q.permission.feature}:${q.permission.action}` : ''}`\n let hit = cache.get(key)\n if (!hit) { hit = resolveModuleAccess(q, ctx); cache.set(key, hit) }\n return hit\n }\n}\n"]}
@@ -0,0 +1,31 @@
1
+ export type ModuleDenyReason = 'not_in_plan' | 'module_off' | 'unit_off' | 'no_permission';
2
+ export interface ModuleAccessQuery {
3
+ plugin: string;
4
+ facet?: string;
5
+ permission?: {
6
+ feature: string;
7
+ action: string;
8
+ };
9
+ }
10
+ export type ModuleAccessDecision = {
11
+ allowed: true;
12
+ } | {
13
+ allowed: false;
14
+ reason: ModuleDenyReason;
15
+ plugin: string;
16
+ facet?: string;
17
+ };
18
+ export interface ModuleAccessContext {
19
+ /** The module (or facet) is on for the account, as the runtime resolved it. */
20
+ isModuleOn: (plugin: string, facet?: string) => boolean;
21
+ /** `plugin` / `plugin.facet` keys the active unit switched off. */
22
+ unitDisabled: ReadonlySet<string>;
23
+ /** The plan includes it. Absent: nothing is gated by plan. */
24
+ entitled?: (plugin: string, facet?: string) => boolean;
25
+ /** The person's role allows it. Absent: nothing is gated by role. */
26
+ can?: (feature: string, action: string) => boolean;
27
+ }
28
+ export declare function resolveModuleAccess(q: ModuleAccessQuery, ctx: ModuleAccessContext): ModuleAccessDecision;
29
+ /** `canUseHere` over one snapshot: every distinct question is answered once. */
30
+ export declare function createModuleAccess(ctx: ModuleAccessContext): (q: ModuleAccessQuery) => ModuleAccessDecision;
31
+ //# sourceMappingURL=module-access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module-access.d.ts","sourceRoot":"","sources":["../../src/access/module-access.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG,YAAY,GAAG,UAAU,GAAG,eAAe,CAAA;AAE1F,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;CACjD;AAED,MAAM,MAAM,oBAAoB,GAC5B;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GACjB;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhF,MAAM,WAAW,mBAAmB;IAClC,+EAA+E;IAC/E,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAA;IACvD,mEAAmE;IACnE,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IACjC,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAA;IACtD,qEAAqE;IACrE,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAA;CACnD;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,iBAAiB,EAAE,GAAG,EAAE,mBAAmB,GAAG,oBAAoB,CAWxG;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,mBAAmB,GAAG,CAAC,CAAC,EAAE,iBAAiB,KAAK,oBAAoB,CAQ3G"}
@@ -53,6 +53,14 @@ export interface AppConfigNavEntry {
53
53
  section: 'main' | 'secondary' | 'settings' | null;
54
54
  position: number | null;
55
55
  }
56
+ /** A module (or one facet of it) a unit switched off for itself (202). A unit
57
+ * with no row inherits the account. */
58
+ export interface AppConfigUnitPlugin {
59
+ unit_id: string;
60
+ plugin: string;
61
+ facet: string | null;
62
+ status: 'active' | 'disabled';
63
+ }
56
64
  export interface AppConfig {
57
65
  tenant_id: string | null;
58
66
  branding?: Record<string, unknown>;
@@ -61,6 +69,8 @@ export interface AppConfig {
61
69
  apps: AppConfigApp[];
62
70
  nav: AppConfigNavEntry[];
63
71
  permissions?: string[];
72
+ /** Per-unit switches, in the same read (202). Absent on an older pool. */
73
+ unit_plugins?: AppConfigUnitPlugin[];
64
74
  generated_at?: string;
65
75
  }
66
76
  /** What a caller with no resolvable tenant gets: nothing, legibly. */
@@ -90,6 +100,11 @@ export declare function normalizeAppConfig(raw: Partial<AppConfig>): AppConfig;
90
100
  * `facets`, because a facet is a part of a plugin and not a plugin.
91
101
  */
92
102
  export declare function toTenantPluginBindings(config: AppConfig): TenantPluginBinding[];
103
+ /**
104
+ * What one unit switched off, as `plugin` / `plugin.facet` keys. Empty for no
105
+ * unit (the whole network) and on a pool without 202.
106
+ */
107
+ export declare function unitDisabledModules(config: Pick<AppConfig, 'unit_plugins'>, unitId: string | null | undefined): Set<string>;
93
108
  /**
94
109
  * Applies the tenant's menu composition to what the plugins offer.
95
110
  *
@@ -1 +1 @@
1
- {"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/app/app-config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/app/app-config.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { Registry } from './chunk-EWKX4VUM.js';
2
- import { errorMessage } from './chunk-ZNXUGLL3.js';
2
+ import { errorMessage } from './chunk-JHKAZA4T.js';
3
3
  import { getAllEntities } from './chunk-CU64QI4A.js';
4
4
  import * as React from 'react';
5
5
 
@@ -1150,7 +1150,7 @@ function resolvePluginRuntime({
1150
1150
  status: isActive ? "active" : binding?.status ?? "disabled",
1151
1151
  isActive,
1152
1152
  activationReason: isActive ? reason : "inactive",
1153
- config: binding?.config ?? {},
1153
+ config: withFacetDefaults(plugin, binding?.config ?? {}),
1154
1154
  missingDependencies: missing,
1155
1155
  settingsTabs: normalizeSettingsTabs(plugin),
1156
1156
  widgets: plugin.widgets ?? [],
@@ -1189,7 +1189,7 @@ function resolvePluginRuntime({
1189
1189
  pluginId: plugin.id,
1190
1190
  icon: entry.icon ?? plugin.icon
1191
1191
  })));
1192
- settingsTabs.push(...plugin.settingsTabs);
1192
+ settingsTabs.push(...plugin.settingsTabs.filter((tab) => hasFacet(tab.requiresFacet)).map((tab) => ({ ...tab, pluginId: tab.pluginId ?? plugin.id })));
1193
1193
  headerControls.push(...(plugin.headerControls ?? []).map((c) => ({ ...c, pluginId: plugin.id })));
1194
1194
  capabilities.push(...plugin.capabilities ?? []);
1195
1195
  events.push(...(plugin.events ?? []).map((e) => ({
@@ -1275,9 +1275,31 @@ function resolvePluginRuntime({
1275
1275
  analyticsCards.sort((a, b) => a.order - b.order);
1276
1276
  return { context, plugins: resolvedPlugins, activePlugins, routes, navigation, settingsTabs, widgets, dashboardWidgets, headerControls, analyticsCards, capabilities, events, aiTools, issues, registries, connectorsByHost, pluginFeatures, pluginLimits, onboardingPrograms, setupGuides };
1277
1277
  }
1278
+ function withFacetDefaults(plugin, config) {
1279
+ const defaults = (plugin.facets ?? []).filter((f) => f.defaultEnabled);
1280
+ if (!defaults.length) return config;
1281
+ const set = config.facets ?? {};
1282
+ const facets = {};
1283
+ for (const f of defaults) facets[f.id] = true;
1284
+ return { ...config, facets: { ...facets, ...set } };
1285
+ }
1286
+ function isModuleOnInRuntime(runtime, key) {
1287
+ const dot = key.indexOf(".");
1288
+ const pluginId = dot < 0 ? key : key.slice(0, dot);
1289
+ const plugin = runtime.activePlugins.find((p) => p.id === pluginId);
1290
+ if (!plugin || plugin.activationReason === "dependency") return false;
1291
+ if (dot < 0) return true;
1292
+ const facet = key.slice(dot + 1);
1293
+ const facets = plugin.config?.facets ?? {};
1294
+ return facets[facet] ?? !!plugin.facets?.find((f) => f.id === facet)?.includedWithPlugin;
1295
+ }
1296
+ function requiredModulesOn(runtime, requires) {
1297
+ if (!requires) return true;
1298
+ return (Array.isArray(requires) ? requires : [requires]).every((key) => isModuleOnInRuntime(runtime, key));
1299
+ }
1278
1300
  function getWidgetsForZone(runtime, zone, contextOverrides) {
1279
1301
  const ctx = { ...runtime.context, ...contextOverrides };
1280
- return runtime.widgets.filter((w) => w.zone === zone && isWidgetVisible(w, ctx));
1302
+ return runtime.widgets.filter((w) => w.zone === zone && isWidgetVisible(w, ctx) && requiredModulesOn(runtime, w.requires));
1281
1303
  }
1282
1304
  function getAnalyticsCards(runtime, options = {}) {
1283
1305
  const ctx = { ...runtime.context, ...options.contextOverrides };
@@ -1303,6 +1325,7 @@ function getDashboardWidgets(runtime, options) {
1303
1325
  if (options.domain && w.domain !== options.domain) return false;
1304
1326
  if (w.verticalId && ctx.tenant?.verticalId && w.verticalId !== ctx.tenant.verticalId) return false;
1305
1327
  if (w.permission && ctx.hasPermission && !ctx.hasPermission(w.permission)) return false;
1328
+ if (!requiredModulesOn(runtime, w.requires)) return false;
1306
1329
  return true;
1307
1330
  });
1308
1331
  }
@@ -1318,6 +1341,6 @@ function useConnectorsForPlugin(hostPluginId) {
1318
1341
  return usePluginRuntime().connectorsByHost.get(hostPluginId) ?? [];
1319
1342
  }
1320
1343
 
1321
- export { BLOCK_SETTING_TYPES, PLUGIN_API_VERSION, PluginRuntimeProvider, auditBlocks, blockRegistry, buildEntityDomainEvents, checkAgainstSchema, checkBlockConstraints, checkBlockContext, checkNodePlacement, clearExtensionPoints, clearHandles, clearLoaders, componentRegistry, dataNeedKey, defaultPropsFromSettings, defineBlock, defineExtensionPoint, defineLoader, definePlugin, describeBlock, describeBlocks, describeExtensionPoints, describeLoaders, entityEventKey, extensionAfterHooks, extensionPointRegistry, formatSchemaIssues, getAnalyticsCards, getBlock, getBlockMeta, getComponent, getDashboardWidgets, getHandle, getMetric, getPage, getPlugin, getPluginFactory, getScaffold, getWidgetsForZone, handleNameFor, handleRegistry, hasExtensionPoint, hasHandle, hasLoader, isBlockEnabledOn, isLoaderParamRef, isLoaderPropRef, listBlocks, listExtensionHandlers, listExtensionPoints, listHandles, listLoaders, listMetrics, listPluginFactories, listPlugins, loaderRegistry, metricRegistry, pageRegistry, pluginFactoryRegistry, pluginRegistry, registerBlock, registerComponent, registerExtensionHandler, registerHandle, registerMetric, registerPage, registerPlugin, registerPluginFactory, registerScaffold, repairBlockTree, resolveHandle, resolveLoader, resolvePluginComponent, resolvePluginRuntime, runEscapeValve, runExtensionPoint, scaffoldRegistry, settingsToJsonSchema, summarizeBlockAudit, unregisterExtensionHandler, useConnectorsForPlugin, usePluginRuntime, usePluginRuntimeOptional };
1322
- //# sourceMappingURL=chunk-5AN766JZ.js.map
1323
- //# sourceMappingURL=chunk-5AN766JZ.js.map
1344
+ export { BLOCK_SETTING_TYPES, PLUGIN_API_VERSION, PluginRuntimeProvider, auditBlocks, blockRegistry, buildEntityDomainEvents, checkAgainstSchema, checkBlockConstraints, checkBlockContext, checkNodePlacement, clearExtensionPoints, clearHandles, clearLoaders, componentRegistry, dataNeedKey, defaultPropsFromSettings, defineBlock, defineExtensionPoint, defineLoader, definePlugin, describeBlock, describeBlocks, describeExtensionPoints, describeLoaders, entityEventKey, extensionAfterHooks, extensionPointRegistry, formatSchemaIssues, getAnalyticsCards, getBlock, getBlockMeta, getComponent, getDashboardWidgets, getHandle, getMetric, getPage, getPlugin, getPluginFactory, getScaffold, getWidgetsForZone, handleNameFor, handleRegistry, hasExtensionPoint, hasHandle, hasLoader, isBlockEnabledOn, isLoaderParamRef, isLoaderPropRef, isModuleOnInRuntime, listBlocks, listExtensionHandlers, listExtensionPoints, listHandles, listLoaders, listMetrics, listPluginFactories, listPlugins, loaderRegistry, metricRegistry, pageRegistry, pluginFactoryRegistry, pluginRegistry, registerBlock, registerComponent, registerExtensionHandler, registerHandle, registerMetric, registerPage, registerPlugin, registerPluginFactory, registerScaffold, repairBlockTree, requiredModulesOn, resolveHandle, resolveLoader, resolvePluginComponent, resolvePluginRuntime, runEscapeValve, runExtensionPoint, scaffoldRegistry, settingsToJsonSchema, summarizeBlockAudit, unregisterExtensionHandler, useConnectorsForPlugin, usePluginRuntime, usePluginRuntimeOptional };
1345
+ //# sourceMappingURL=chunk-5NVZ2NOO.js.map
1346
+ //# sourceMappingURL=chunk-5NVZ2NOO.js.map