@fayz-ai/core 0.14.0 → 0.16.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 (100) hide show
  1. package/dist/access/engine.d.ts +31 -1
  2. package/dist/access/engine.d.ts.map +1 -1
  3. package/dist/access/index.d.ts +1 -1
  4. package/dist/access/index.d.ts.map +1 -1
  5. package/dist/access/index.js +24 -3
  6. package/dist/access/index.js.map +1 -1
  7. package/dist/app/app-config.d.ts +101 -0
  8. package/dist/app/app-config.d.ts.map +1 -0
  9. package/dist/app/service-client.d.ts +69 -0
  10. package/dist/app/service-client.d.ts.map +1 -0
  11. package/dist/{chunk-S5FOFKR4.js → chunk-2ZAUJVYP.js} +103 -24
  12. package/dist/chunk-2ZAUJVYP.js.map +1 -0
  13. package/dist/{chunk-KFBEKFII.js → chunk-BCI743RH.js} +37 -4
  14. package/dist/chunk-BCI743RH.js.map +1 -0
  15. package/dist/{chunk-IKZR3WSK.js → chunk-KJUD6JA5.js} +3 -3
  16. package/dist/{chunk-IKZR3WSK.js.map → chunk-KJUD6JA5.js.map} +1 -1
  17. package/dist/{chunk-7GJTLBG3.js → chunk-OHH26S4N.js} +3 -3
  18. package/dist/{chunk-7GJTLBG3.js.map → chunk-OHH26S4N.js.map} +1 -1
  19. package/dist/chunk-RCMFU3RZ.js +13 -0
  20. package/dist/chunk-RCMFU3RZ.js.map +1 -0
  21. package/dist/{chunk-RRYFXUQX.js → chunk-UMIZDILP.js} +7 -3
  22. package/dist/chunk-UMIZDILP.js.map +1 -0
  23. package/dist/{chunk-X4FRQR2R.js → chunk-ZIB6Y7TE.js} +153 -8
  24. package/dist/chunk-ZIB6Y7TE.js.map +1 -0
  25. package/dist/data/archetype.d.ts +5 -0
  26. package/dist/data/archetype.d.ts.map +1 -1
  27. package/dist/data/cached.d.ts +17 -2
  28. package/dist/data/cached.d.ts.map +1 -1
  29. package/dist/data/index.d.ts +1 -0
  30. package/dist/data/index.d.ts.map +1 -1
  31. package/dist/data/index.js +2 -2
  32. package/dist/data/missing-objects.d.ts +14 -0
  33. package/dist/data/missing-objects.d.ts.map +1 -0
  34. package/dist/data/offline.d.ts +10 -1
  35. package/dist/data/offline.d.ts.map +1 -1
  36. package/dist/data/resolve.d.ts.map +1 -1
  37. package/dist/data/supabase.d.ts +5 -0
  38. package/dist/data/supabase.d.ts.map +1 -1
  39. package/dist/data/unit-scope.d.ts +48 -0
  40. package/dist/data/unit-scope.d.ts.map +1 -0
  41. package/dist/events/envelope.d.ts.map +1 -1
  42. package/dist/events/index.d.ts +2 -0
  43. package/dist/events/index.d.ts.map +1 -1
  44. package/dist/events/log.d.ts.map +1 -1
  45. package/dist/events/useDataVersion.d.ts +28 -0
  46. package/dist/events/useDataVersion.d.ts.map +1 -0
  47. package/dist/i18n/index.d.ts.map +1 -1
  48. package/dist/i18n/index.js +1 -1
  49. package/dist/i18n/shell-translations.en.d.ts.map +1 -1
  50. package/dist/i18n/shell-translations.pt-br.d.ts.map +1 -1
  51. package/dist/index.d.ts +15 -2
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +653 -45
  54. package/dist/index.js.map +1 -1
  55. package/dist/integrations/index.js +2 -2
  56. package/dist/lib/debug.d.ts +12 -0
  57. package/dist/lib/debug.d.ts.map +1 -0
  58. package/dist/lib/unit.d.ts +20 -0
  59. package/dist/lib/unit.d.ts.map +1 -0
  60. package/dist/manifest/index.d.ts +45 -0
  61. package/dist/manifest/index.d.ts.map +1 -1
  62. package/dist/manifest/integration-surface.d.ts +16 -0
  63. package/dist/manifest/integration-surface.d.ts.map +1 -0
  64. package/dist/messaging/index.d.ts +5 -2
  65. package/dist/messaging/index.d.ts.map +1 -1
  66. package/dist/messaging/phone.d.ts +32 -0
  67. package/dist/messaging/phone.d.ts.map +1 -0
  68. package/dist/messaging/provider-template.d.ts +122 -0
  69. package/dist/messaging/provider-template.d.ts.map +1 -0
  70. package/dist/messaging/registry.d.ts +12 -1
  71. package/dist/messaging/registry.d.ts.map +1 -1
  72. package/dist/messaging/types.d.ts +32 -0
  73. package/dist/messaging/types.d.ts.map +1 -1
  74. package/dist/plugin/index.js +1 -1
  75. package/dist/plugin/runtime.d.ts.map +1 -1
  76. package/dist/search/engine.d.ts.map +1 -1
  77. package/dist/{shell-translations.pt-br-PTOWYXNA.js → shell-translations.pt-br-HD4F3QKU.js} +118 -7
  78. package/dist/shell-translations.pt-br-HD4F3QKU.js.map +1 -0
  79. package/dist/testing/index.js +3 -3
  80. package/dist/types/agents.d.ts +97 -0
  81. package/dist/types/agents.d.ts.map +1 -0
  82. package/dist/types/analytics.d.ts +15 -0
  83. package/dist/types/analytics.d.ts.map +1 -1
  84. package/dist/types/auth.d.ts +9 -0
  85. package/dist/types/auth.d.ts.map +1 -1
  86. package/dist/types/crud.d.ts +38 -0
  87. package/dist/types/crud.d.ts.map +1 -1
  88. package/dist/types/index.d.ts +2 -0
  89. package/dist/types/index.d.ts.map +1 -1
  90. package/dist/types/index.js +1 -1
  91. package/dist/types/org.d.ts +10 -0
  92. package/dist/types/org.d.ts.map +1 -1
  93. package/dist/types/plugins.d.ts +97 -0
  94. package/dist/types/plugins.d.ts.map +1 -1
  95. package/package.json +2 -2
  96. package/dist/chunk-KFBEKFII.js.map +0 -1
  97. package/dist/chunk-RRYFXUQX.js.map +0 -1
  98. package/dist/chunk-S5FOFKR4.js.map +0 -1
  99. package/dist/chunk-X4FRQR2R.js.map +0 -1
  100. package/dist/shell-translations.pt-br-PTOWYXNA.js.map +0 -1
@@ -12,6 +12,19 @@ export interface AccessSubject {
12
12
  profile: PermissionProfile | null;
13
13
  /** The tenant's currently-active plan. */
14
14
  plan: Plan | null;
15
+ /**
16
+ * Extra role a person has INSIDE a given unit, keyed by unit id.
17
+ *
18
+ * Additive, never subtractive: the org role applies everywhere and a unit role
19
+ * only adds inside its unit. That choice is what makes this shippable — the
20
+ * answer without a unit is a strict SUBSET of the answer with one, so every
21
+ * existing call site keeps its exact meaning and none of them had to change.
22
+ * The alternative (a unit role that overrides) can take access away by
23
+ * accident, and every screen would have to explain which role won.
24
+ */
25
+ unitProfiles?: Record<string, PermissionProfile>;
26
+ /** The unit the person is looking at, used when a check does not name one. */
27
+ activeUnitId?: string;
15
28
  }
16
29
  /**
17
30
  * The org owner has every permission implicitly. Owner is represented by the
@@ -41,6 +54,21 @@ export declare function profileHasSystemPermission(profile: PermissionProfile |
41
54
  * open rather than silently locking.
42
55
  */
43
56
  export declare function isEntitledByPlan(plan: Plan | null, feature: string): boolean;
57
+ /**
58
+ * Union two profiles. Grants merge per feature, system permissions merge, and
59
+ * the result is never smaller than either input — the additive rule, in code.
60
+ *
61
+ * `manage` is not expanded here: {@link profileHasPermission} already treats it
62
+ * as satisfying any action, so expanding it would be a second implementation of
63
+ * the same rule waiting to disagree with the first.
64
+ */
65
+ export declare function mergeProfiles(base: PermissionProfile | null, overlay: PermissionProfile | null): PermissionProfile | null;
66
+ /**
67
+ * The profile that applies where the person is standing: their org role, plus
68
+ * whatever the unit adds. With no unit — or no binding in it — this is exactly
69
+ * the org role, which is why nothing that predates units changes behaviour.
70
+ */
71
+ export declare function effectiveProfileFor(subject: AccessSubject, unitId?: string): PermissionProfile | null;
44
72
  /**
45
73
  * The single access decision: role FIRST, then plan.
46
74
  *
@@ -50,7 +78,9 @@ export declare function isEntitledByPlan(plan: Plan | null, feature: string): bo
50
78
  * 2. Plan — even the owner does NOT bypass the plan: upgrading is the tenant's
51
79
  * decision, not a role power. Denied → `{ allowed:false, reason:'plan' }`.
52
80
  */
53
- export declare function resolveAccess(subject: AccessSubject, feature: string, action?: PermissionAction): AccessDecision;
81
+ export declare function resolveAccess(subject: AccessSubject, feature: string, action?: PermissionAction, options?: {
82
+ unitId?: string;
83
+ }): AccessDecision;
54
84
  /**
55
85
  * Pure numeric side of a limit: given the plan cap, whether a countable
56
86
  * declaration exists, and the current usage, derive the limit math. Unlimited
@@ -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;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAElE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,iBAAiB,GAAG,IAAI,EACjC,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAeT;AAED;;;;;GAKG;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;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,gBAAgB,GACxB,cAAc,CAQhB;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;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,iBAAiB,GAAG,IAAI,EACjC,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAeT;AAED;;;;;GAKG;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;;;;;;;;GAQG;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,4 +1,4 @@
1
- export { isOwnerProfile, profileHasPermission, profileHasSystemPermission, isEntitledByPlan, resolveAccess, resolveLimit, } from './engine';
1
+ export { isOwnerProfile, profileHasPermission, profileHasSystemPermission, isEntitledByPlan, resolveAccess, resolveLimit, mergeProfiles, effectiveProfileFor, } from './engine';
2
2
  export type { DenyReason, AccessDecision, AccessSubject } from './engine';
3
3
  export { UPGRADE_URL, agentDenial } from './denial';
4
4
  export type { AgentDenial, AgentDenialLimit, AgentGuardResult } from './denial';
@@ -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,GACb,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"}
@@ -18,8 +18,29 @@ function profileHasSystemPermission(profile, permission) {
18
18
  function isEntitledByPlan(plan, feature) {
19
19
  return plan?.entitlements?.features?.[feature] !== false;
20
20
  }
21
- function resolveAccess(subject, feature, action) {
22
- if (!profileHasPermission(subject.profile, feature, action)) {
21
+ function mergeProfiles(base, overlay) {
22
+ if (!overlay) return base;
23
+ if (!base) return overlay;
24
+ if (isOwnerProfile(base)) return base;
25
+ const grants = { ...base.grants };
26
+ for (const [feature, actions] of Object.entries(overlay.grants ?? {})) {
27
+ const merged = /* @__PURE__ */ new Set([...grants[feature] ?? [], ...actions]);
28
+ grants[feature] = [...merged];
29
+ }
30
+ const system = /* @__PURE__ */ new Set([...base.systemPermissions ?? [], ...overlay.systemPermissions ?? []]);
31
+ return {
32
+ ...base,
33
+ grants,
34
+ systemPermissions: system.size > 0 ? [...system] : void 0
35
+ };
36
+ }
37
+ function effectiveProfileFor(subject, unitId) {
38
+ if (!unitId) return subject.profile;
39
+ return mergeProfiles(subject.profile, subject.unitProfiles?.[unitId] ?? null);
40
+ }
41
+ function resolveAccess(subject, feature, action, options) {
42
+ const profile = effectiveProfileFor(subject, options?.unitId ?? subject.activeUnitId);
43
+ if (!profileHasPermission(profile, feature, action)) {
23
44
  return { allowed: false, reason: "role" };
24
45
  }
25
46
  if (!isEntitledByPlan(subject.plan, feature)) {
@@ -51,6 +72,6 @@ function mergeLimitDeclarations(...layers) {
51
72
  return Array.from(byKey.values());
52
73
  }
53
74
 
54
- export { UPGRADE_URL, agentDenial, isEntitledByPlan, isOwnerProfile, mergeLimitDeclarations, profileHasPermission, profileHasSystemPermission, resolveAccess, resolveLimit };
75
+ export { UPGRADE_URL, agentDenial, effectiveProfileFor, isEntitledByPlan, isOwnerProfile, mergeLimitDeclarations, mergeProfiles, profileHasPermission, profileHasSystemPermission, resolveAccess, resolveLimit };
55
76
  //# sourceMappingURL=index.js.map
56
77
  //# 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\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 */\nexport function profileHasPermission(\n profile: PermissionProfile | null,\n feature: string,\n action?: string,\n): boolean {\n if (!profile) return true\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.\n */\nexport function profileHasSystemPermission(\n profile: PermissionProfile | null,\n permission: SystemPermission,\n): boolean {\n if (!profile) return true\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 * 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 */\nexport function resolveAccess(\n subject: AccessSubject,\n feature: string,\n action?: PermissionAction,\n): AccessDecision {\n if (!profileHasPermission(subject.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"],"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 */\nexport function profileHasPermission(\n profile: PermissionProfile | null,\n feature: string,\n action?: string,\n): boolean {\n if (!profile) return true\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.\n */\nexport function profileHasSystemPermission(\n profile: PermissionProfile | null,\n permission: SystemPermission,\n): boolean {\n if (!profile) return true\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 */\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"]}
@@ -0,0 +1,101 @@
1
+ import type { TenantPluginBinding } from '../types/plugins';
2
+ /** One activation row: a plugin, or one facet of it. */
3
+ export interface AppConfigPlugin {
4
+ plugin: string;
5
+ /** null for the plugin itself; a name for one of its parts. */
6
+ facet: string | null;
7
+ status: 'active' | 'disabled';
8
+ /** Who turned it on — manual, plan, trial, agent, default. */
9
+ source: string;
10
+ config: Record<string, unknown>;
11
+ /** When a trial or courtesy ends; null is forever. */
12
+ expires_at: string | null;
13
+ }
14
+ /** The tenant's difference over the menu the plugins offer. */
15
+ export interface AppConfigNavEntry {
16
+ key: string;
17
+ hidden: boolean;
18
+ label: string | null;
19
+ section: 'main' | 'secondary' | 'settings' | null;
20
+ position: number | null;
21
+ }
22
+ export interface AppConfig {
23
+ tenant_id: string | null;
24
+ branding?: Record<string, unknown>;
25
+ settings?: Record<string, unknown>;
26
+ plugins: AppConfigPlugin[];
27
+ nav: AppConfigNavEntry[];
28
+ permissions?: string[];
29
+ generated_at?: string;
30
+ }
31
+ /** What a caller with no resolvable tenant gets: nothing, legibly. */
32
+ export declare const EMPTY_APP_CONFIG: AppConfig;
33
+ /**
34
+ * Reads the whole configuration in one round trip.
35
+ *
36
+ * Never throws: a shell that cannot render because the configuration call
37
+ * failed is worse than a shell that renders the defaults. The failure is
38
+ * returned as an empty configuration, which is the same thing the RPC returns
39
+ * for a caller with no tenant — and both are visible in one place rather than
40
+ * as a blank screen.
41
+ */
42
+ export declare function fetchAppConfig(client: unknown, options?: {
43
+ tenantId?: string;
44
+ force?: boolean;
45
+ }): Promise<AppConfig>;
46
+ /** Drops what is cached — called after any write through the config RPCs. */
47
+ export declare function invalidateAppConfig(tenantId?: string): void;
48
+ export declare function normalizeAppConfig(raw: Partial<AppConfig>): AppConfig;
49
+ /**
50
+ * The activation rows as the plugin runtime already understands them.
51
+ *
52
+ * `resolvePluginRuntime` has accepted `tenantPlugins: TenantPluginBinding[]`
53
+ * since it was written and nothing ever supplied them from anywhere but a file.
54
+ * This is that supply — facet rows are folded into their plugin's config under
55
+ * `facets`, because a facet is a part of a plugin and not a plugin.
56
+ */
57
+ export declare function toTenantPluginBindings(config: AppConfig): TenantPluginBinding[];
58
+ /**
59
+ * Applies the tenant's menu composition to what the plugins offer.
60
+ *
61
+ * Only the difference is stored, so an entry with no row here keeps exactly the
62
+ * position and label its plugin gave it — which is what lets a plugin ship a new
63
+ * entry and have it reach every tenant.
64
+ */
65
+ export declare function applyNavComposition<T extends {
66
+ key: string;
67
+ label?: string;
68
+ section?: string;
69
+ position?: number;
70
+ }>(entries: T[], nav: AppConfigNavEntry[]): T[];
71
+ /** The keys a generated app must NOT ship in a file: they are per-tenant. */
72
+ export declare const TENANT_OWNED_MANIFEST_KEYS: readonly ["theme", "branding", "locale"];
73
+ export interface ManifestLike {
74
+ [key: string]: unknown;
75
+ theme?: Record<string, unknown>;
76
+ branding?: Record<string, unknown>;
77
+ locale?: Record<string, unknown>;
78
+ surfaces?: Record<string, {
79
+ plugins?: Array<{
80
+ id: string;
81
+ config?: Record<string, unknown>;
82
+ }>;
83
+ } & Record<string, unknown>>;
84
+ }
85
+ /**
86
+ * Folds the tenant's configuration over the bundle's manifest.
87
+ *
88
+ * What stays the manifest's: the surfaces, their scaffolds, and which plugin
89
+ * ids the bundle can load at all — an import-graph fact that no database row can
90
+ * change, because the code either shipped or it did not.
91
+ *
92
+ * What the database decides: branding, locale, and whether each of those
93
+ * plugins is on for this tenant, with its settings. That is the whole point of
94
+ * ADR 0023 — eleven customers stop needing eleven repositories, and turning a
95
+ * feature on stops being a deploy.
96
+ *
97
+ * An empty config changes nothing, so a pool that has not run migration 150 yet
98
+ * renders exactly what it rendered before.
99
+ */
100
+ export declare function applyAppConfigToManifest<T extends ManifestLike>(manifest: T, config: AppConfig): T;
101
+ //# sourceMappingURL=app-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/app/app-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ /** Why a call did not produce a value. Each one has a different right answer. */
2
+ export type ServiceRefusal =
3
+ /** The service is not registered, or was turned off. */
4
+ 'unknown_service'
5
+ /** No tenant in scope — a caller that cannot be placed. */
6
+ | 'no_tenant'
7
+ /** The caller lacks the permission the service requires. */
8
+ | 'forbidden'
9
+ /** The tenant has not contracted it: an upsell, not an error. */
10
+ | 'not_contracted'
11
+ /** It did not answer in time. */
12
+ | 'timeout'
13
+ /** It answered with a failure. */
14
+ | 'failed'
15
+ /** There is nothing to call through (no client, no session). */
16
+ | 'unavailable';
17
+ export interface ServiceSuccess<T> {
18
+ ok: true;
19
+ data: T;
20
+ durationMs: number;
21
+ }
22
+ export interface ServiceFailure {
23
+ ok: false;
24
+ reason: ServiceRefusal;
25
+ /** What the user can still do, as the service itself declared it. */
26
+ manualFallback?: string;
27
+ /** For `not_contracted`: what to offer. */
28
+ entitlement?: string;
29
+ plugin?: string;
30
+ /** For `forbidden`: what would be needed. */
31
+ permission?: string;
32
+ message?: string;
33
+ }
34
+ export type ServiceResult<T> = ServiceSuccess<T> | ServiceFailure;
35
+ export interface CallServiceOptions {
36
+ /** Overrides the timeout the registry declares for this service. */
37
+ timeoutMs?: number;
38
+ /** The edge function that performs the HTTP leg. Defaults to 'service-router'. */
39
+ router?: string;
40
+ /**
41
+ * Calls on behalf of a tenant with no session — the event consumer's 2am
42
+ * reaction (#278). Requires a service-role client; a browser client passing
43
+ * this is refused by the router, not by trust.
44
+ */
45
+ tenantId?: string;
46
+ }
47
+ /**
48
+ * Calls a service by name.
49
+ *
50
+ * Two round trips by design. The first asks the database whether the call is
51
+ * allowed — which costs nothing and answers the same way everywhere, so a
52
+ * refusal never leaves the cluster and an unentitled tenant never reaches the
53
+ * service at all. Only an allowed call makes the second.
54
+ */
55
+ export declare function callService<T = unknown>(client: unknown, name: string, payload?: Record<string, unknown>, options?: CallServiceOptions): Promise<ServiceResult<T>>;
56
+ /**
57
+ * Calls a service for a tenant with no user present — the worker draining the
58
+ * event log (#277) reacting to a fact at 2am (#278).
59
+ *
60
+ * Same door, same refusals, same never-throws contract. What differs is who is
61
+ * asking: there is no person, so there is no permission to check, and the
62
+ * entitlement rule carries the whole decision because a tenant's contract is a
63
+ * property of the tenant rather than of who is looking.
64
+ *
65
+ * The client must hold the service role. A browser client that passes a tenant
66
+ * is refused by the router, which checks rather than trusts.
67
+ */
68
+ export declare function callServiceForTenant<T = unknown>(client: unknown, tenantId: string, name: string, payload?: Record<string, unknown>, options?: Omit<CallServiceOptions, 'tenantId'>): Promise<ServiceResult<T>>;
69
+ //# sourceMappingURL=service-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-client.d.ts","sourceRoot":"","sources":["../../src/app/service-client.ts"],"names":[],"mappings":"AAgBA,iFAAiF;AACjF,MAAM,MAAM,cAAc;AACxB,wDAAwD;AACtD,iBAAiB;AACnB,2DAA2D;GACzD,WAAW;AACb,4DAA4D;GAC1D,WAAW;AACb,iEAAiE;GAC/D,gBAAgB;AAClB,iCAAiC;GAC/B,SAAS;AACX,kCAAkC;GAChC,QAAQ;AACV,gEAAgE;GAC9D,aAAa,CAAA;AAEjB,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,EAAE,EAAE,IAAI,CAAA;IACR,IAAI,EAAE,CAAC,CAAA;IACP,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,KAAK,CAAA;IACT,MAAM,EAAE,cAAc,CAAA;IACtB,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,cAAc,CAAC,CAAC,CAAC,GAAG,cAAc,CAAA;AAajE,MAAM,WAAW,kBAAkB;IACjC,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,CAAC,GAAG,OAAO,EAC3C,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACrC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CA6D3B;AA2CD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,GAAG,OAAO,EAC9C,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACrC,OAAO,GAAE,IAAI,CAAC,kBAAkB,EAAE,UAAU,CAAM,GACjD,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAE3B"}
@@ -1,4 +1,4 @@
1
- import { globalCache, getActiveTenantId, getSupabaseClientOptional, parseRangeFilter, matchesRange, applyRangeToQuery, isFilterObject, stableKey, DEFAULT_CACHE_TTL, createSupabaseProvider } from './chunk-KFBEKFII.js';
1
+ import { globalCache, getActiveTenantId, getSupabaseClientOptional, parseRangeFilter, matchesRange, applyUnitScope, applyRangeToQuery, isFilterObject, stableKey, DEFAULT_CACHE_TTL, getScopedResource, createSupabaseProvider, resolveUnitScope, clearGlobalCache } from './chunk-BCI743RH.js';
2
2
  import { declaredCustomFields, validateEntityRecordData } from './chunk-EWKX4VUM.js';
3
3
  import * as React from 'react';
4
4
 
@@ -303,14 +303,15 @@ var ARCHETYPE_CONFIG = {
303
303
  };
304
304
  var CUSTOM_FIELDS_COLUMN = "custom_fields";
305
305
  var CUSTOM_FIELD_FILTER_PREFIX = "custom.";
306
+ var UNIT_COLUMN = "unit_id";
306
307
  var ARCHETYPE_COLUMNS = {
307
- person: /* @__PURE__ */ new Set(["name", "email", "phone", "document_number", "avatar_url", "date_of_birth", "address", "city", "state", "country", "postal_code", "tags", "is_active", "notes", "metadata", CUSTOM_FIELDS_COLUMN]),
308
- category: /* @__PURE__ */ new Set(["name", "slug", "parent_id", "icon", "color", "sort_order", "is_active", "metadata", CUSTOM_FIELDS_COLUMN]),
309
- product: /* @__PURE__ */ new Set(["name", "description", "sku", "price", "cost", "currency", "unit", "image_url", "stock", "min_stock", "status", "is_active", "tags", "metadata", "category_id", CUSTOM_FIELDS_COLUMN]),
310
- service: /* @__PURE__ */ new Set(["name", "description", "price", "cost", "currency", "duration_minutes", "image_url", "status", "is_active", "tags", "metadata", "category_id", CUSTOM_FIELDS_COLUMN]),
308
+ person: /* @__PURE__ */ new Set(["name", "email", "phone", "document_number", "avatar_url", "date_of_birth", "address", "city", "state", "country", "postal_code", "tags", "is_active", "is_bookable", "notes", "metadata", CUSTOM_FIELDS_COLUMN, UNIT_COLUMN]),
309
+ category: /* @__PURE__ */ new Set(["name", "slug", "parent_id", "icon", "color", "sort_order", "is_active", "metadata", CUSTOM_FIELDS_COLUMN, UNIT_COLUMN]),
310
+ product: /* @__PURE__ */ new Set(["name", "description", "sku", "price", "cost", "currency", "unit", "image_url", "stock", "min_stock", "status", "is_active", "tags", "metadata", "category_id", CUSTOM_FIELDS_COLUMN, UNIT_COLUMN]),
311
+ service: /* @__PURE__ */ new Set(["name", "description", "price", "cost", "currency", "duration_minutes", "image_url", "status", "is_active", "tags", "metadata", "category_id", CUSTOM_FIELDS_COLUMN, UNIT_COLUMN]),
311
312
  location: /* @__PURE__ */ new Set(["name", "email", "phone", "address", "city", "state", "country", "postal_code", "is_headquarters", "is_active", "tags", "notes", "metadata", CUSTOM_FIELDS_COLUMN]),
312
313
  order: /* @__PURE__ */ new Set(["reference_number", "status", "party_id", "assignee_id", "location_id", "subtotal", "discount", "tax", "total", "currency", "due_at", "completed_at", "notes", "tags", "metadata", CUSTOM_FIELDS_COLUMN]),
313
- transaction: /* @__PURE__ */ new Set(["order_id", "party_id", "amount", "currency", "payment_method", "reference", "status", "transacted_at", "notes", "metadata", CUSTOM_FIELDS_COLUMN]),
314
+ transaction: /* @__PURE__ */ new Set(["order_id", "party_id", "amount", "currency", "payment_method", "reference", "status", "transacted_at", "notes", "metadata", CUSTOM_FIELDS_COLUMN, UNIT_COLUMN]),
314
315
  schedule: /* @__PURE__ */ new Set(["assignee_id", "location_id", "day_of_week", "specific_date", "starts_at", "ends_at", "is_active", "metadata", CUSTOM_FIELDS_COLUMN])
315
316
  };
316
317
  function camelToSnake(str) {
@@ -338,11 +339,14 @@ function createArchetypeProvider(config) {
338
339
  function coreClient() {
339
340
  return getClient2();
340
341
  }
342
+ function unitScope() {
343
+ return resolveUnitScope(config);
344
+ }
341
345
  function splitFields(data) {
342
346
  const archetypeData = {};
343
347
  const projectData = {};
344
348
  for (const [key, value] of Object.entries(data)) {
345
- if (["id", "createdAt", "updatedAt", "createdBy", "tenantId", fkColumn].includes(key)) continue;
349
+ if (["id", "createdAt", "updatedAt", "createdBy", "ownerId", "tenantId", fkColumn].includes(key)) continue;
346
350
  const snakeKey = camelToSnake(key);
347
351
  if (archetypeColumns.has(snakeKey)) {
348
352
  archetypeData[snakeKey] = value;
@@ -364,6 +368,7 @@ function createArchetypeProvider(config) {
364
368
  if (!tenantId) return { data: [], total: 0 };
365
369
  const countOpts = query.countMode === "none" ? void 0 : { count: "exact" };
366
370
  let q = isPure ? coreClient().from(ac.table).select("*", countOpts).eq("tenant_id", tenantId) : getClient2().from(viewName).select("*", countOpts).eq("tenant_id", tenantId);
371
+ q = applyUnitScope(q, unitScope());
367
372
  if (isPure && HAS_KIND.has(config.archetype)) {
368
373
  q = q.eq("kind", config.archetypeKind);
369
374
  }
@@ -413,6 +418,10 @@ function createArchetypeProvider(config) {
413
418
  if (HAS_KIND.has(config.archetype)) {
414
419
  archetypeData.kind = config.archetypeKind;
415
420
  }
421
+ const scope = unitScope();
422
+ if (scope && archetypeData[scope.column] === void 0) {
423
+ archetypeData[scope.column] = scope.id;
424
+ }
416
425
  const { data: archetypeRow, error: archetypeError } = await coreClient().from(ac.table).insert(archetypeData).select().single();
417
426
  if (archetypeError) throw archetypeError;
418
427
  const archetypeId = archetypeRow.id;
@@ -465,8 +474,9 @@ function createArchetypeProvider(config) {
465
474
  function cachePrefixFor(tenantId, table) {
466
475
  return `${tenantId ?? "_"}:${table}`;
467
476
  }
468
- function cacheKeyFor(tenantId, table, query) {
469
- return `${cachePrefixFor(tenantId, table)}:${stableKey(query)}`;
477
+ function cacheKeyFor(tenantId, table, query, unitId) {
478
+ const keyed = unitId ? { ...query, __unit: unitId } : query;
479
+ return `${cachePrefixFor(tenantId, table)}:${stableKey(keyed)}`;
470
480
  }
471
481
  function withCache(provider, options) {
472
482
  function prefix() {
@@ -474,7 +484,7 @@ function withCache(provider, options) {
474
484
  }
475
485
  return {
476
486
  async list(query) {
477
- const key = cacheKeyFor(options.tenantId(), options.table, query);
487
+ const key = cacheKeyFor(options.tenantId(), options.table, query, options.unitId?.());
478
488
  const cached = globalCache.get(key);
479
489
  if (cached) return { ...cached, cached: true };
480
490
  const result = await provider.list(query);
@@ -1034,8 +1044,8 @@ var REPLICA_MAX_ROWS = 5e3;
1034
1044
  var REPLICA_FIRST_DELAY_MS = 1500;
1035
1045
  var REPLICA_REFRESH_DELAY_MS = 3e3;
1036
1046
  var REPLICA_MIN_INTERVAL_MS = 6e4;
1037
- function replicaKeyFor(tenantId, table) {
1038
- return `replica:${cachePrefixFor(tenantId, table)}`;
1047
+ function replicaKeyFor(tenantId, table, unitId) {
1048
+ return `replica:${cachePrefixFor(tenantId, table)}${unitId ? `@${unitId}` : ""}`;
1039
1049
  }
1040
1050
  function withOffline(provider, options) {
1041
1051
  if (options.policy === "online") return provider;
@@ -1044,14 +1054,14 @@ function withOffline(provider, options) {
1044
1054
  async function fromPersisted(query) {
1045
1055
  const l2 = getPersistentCache();
1046
1056
  if (!l2?.available) return void 0;
1047
- const entry = await l2.getStale(cacheKeyFor(options.tenantId(), options.table, query));
1057
+ const entry = await l2.getStale(cacheKeyFor(options.tenantId(), options.table, query, options.unitId?.()));
1048
1058
  if (!entry) return void 0;
1049
1059
  return { ...entry.data, stale: true, staleAt: entry.storedAt };
1050
1060
  }
1051
1061
  async function fromReplica(query) {
1052
1062
  const l2 = getPersistentCache();
1053
1063
  if (!l2?.available) return void 0;
1054
- const snapshot = await l2.getReplica(replicaKeyFor(options.tenantId(), options.table));
1064
+ const snapshot = await l2.getReplica(replicaKeyFor(options.tenantId(), options.table, options.unitId?.()));
1055
1065
  if (!snapshot) return void 0;
1056
1066
  const result = await localList(snapshot.rows, query, options.entityDef);
1057
1067
  return { ...result, stale: true, staleAt: snapshot.syncedAt };
@@ -1062,7 +1072,7 @@ function withOffline(provider, options) {
1062
1072
  const result = await provider.list(query);
1063
1073
  const l2 = getPersistentCache();
1064
1074
  if (options.policy === "cache" && l2?.available) {
1065
- l2.put(cacheKeyFor(options.tenantId(), options.table, query), result, Date.now() + ttl);
1075
+ l2.put(cacheKeyFor(options.tenantId(), options.table, query, options.unitId?.()), result, Date.now() + ttl);
1066
1076
  }
1067
1077
  return result;
1068
1078
  } catch (err) {
@@ -1118,13 +1128,13 @@ async function syncReplica(provider, options) {
1118
1128
  if (result.data.length < pageSize) break;
1119
1129
  }
1120
1130
  const snapshot = rows.slice(0, maxRows);
1121
- l2.putReplica(replicaKeyFor(options.tenantId, options.table), snapshot);
1131
+ l2.putReplica(replicaKeyFor(options.tenantId, options.table, options.unitId), snapshot);
1122
1132
  await l2.flush();
1123
1133
  return snapshot.length;
1124
1134
  }
1125
1135
  var replicaSyncs = /* @__PURE__ */ new Map();
1126
1136
  function ensureReplicaSync(provider, options) {
1127
- const key = replicaKeyFor(options.tenantId(), options.table);
1137
+ const key = replicaKeyFor(options.tenantId(), options.table, options.unitId?.());
1128
1138
  if (replicaSyncs.has(key)) return;
1129
1139
  const state = {
1130
1140
  // Tenant read at sync time, not at registration: the first provider is often
@@ -1132,6 +1142,7 @@ function ensureReplicaSync(provider, options) {
1132
1142
  run: () => syncReplica(provider, {
1133
1143
  table: options.table,
1134
1144
  tenantId: options.tenantId(),
1145
+ unitId: options.unitId?.(),
1135
1146
  pageSize: options.replicaPageSize,
1136
1147
  maxRows: options.replicaMaxRows
1137
1148
  }),
@@ -1149,7 +1160,7 @@ function ensureReplicaSync(provider, options) {
1149
1160
  scheduleReplicaSync(options, REPLICA_FIRST_DELAY_MS);
1150
1161
  }
1151
1162
  function scheduleReplicaSync(options, delayMs) {
1152
- const state = replicaSyncs.get(replicaKeyFor(options.tenantId(), options.table));
1163
+ const state = replicaSyncs.get(replicaKeyFor(options.tenantId(), options.table, options.unitId?.()));
1153
1164
  if (!state || state.timer) return;
1154
1165
  state.timer = setTimeout(() => {
1155
1166
  state.timer = null;
@@ -1247,6 +1258,47 @@ function mergeCustomFields(current, patch) {
1247
1258
  }
1248
1259
  return merged;
1249
1260
  }
1261
+ var _activeUnitId;
1262
+ var _unitMode = false;
1263
+ var listeners = /* @__PURE__ */ new Set();
1264
+ function notify() {
1265
+ clearGlobalCache();
1266
+ for (const l of listeners) l();
1267
+ }
1268
+ function setActiveUnitId(id) {
1269
+ if (id === _activeUnitId) return;
1270
+ _activeUnitId = id;
1271
+ notify();
1272
+ }
1273
+ function getActiveUnitId() {
1274
+ return _unitMode ? _activeUnitId : void 0;
1275
+ }
1276
+ function setUnitMode(on) {
1277
+ if (on === _unitMode) return;
1278
+ _unitMode = on;
1279
+ notify();
1280
+ }
1281
+ function isUnitMode() {
1282
+ return _unitMode;
1283
+ }
1284
+ function subscribeActiveUnit(listener) {
1285
+ listeners.add(listener);
1286
+ return () => {
1287
+ listeners.delete(listener);
1288
+ };
1289
+ }
1290
+ function useActiveUnitId() {
1291
+ return React.useSyncExternalStore(subscribeActiveUnit, getActiveUnitId, getActiveUnitId);
1292
+ }
1293
+ function useUnitMode() {
1294
+ return React.useSyncExternalStore(subscribeActiveUnit, isUnitMode, isUnitMode);
1295
+ }
1296
+ function resetActiveUnit() {
1297
+ if (_activeUnitId === void 0 && !_unitMode) return;
1298
+ _activeUnitId = void 0;
1299
+ _unitMode = false;
1300
+ notify();
1301
+ }
1250
1302
 
1251
1303
  // src/data/custom-fields.ts
1252
1304
  var KEY = "customFields";
@@ -1311,10 +1363,20 @@ function resolveDataProvider(entityDef, mockData, options = {}) {
1311
1363
  const client = getSupabaseClientOptional();
1312
1364
  if (client && entityDef.data?.table) {
1313
1365
  const tenantId = () => getActiveTenantId();
1366
+ const registered = getScopedResource(entityDef.data.table);
1367
+ const unitScoped = entityDef.data.unitScoped ?? Boolean(registered);
1368
+ const unitId = unitScoped ? () => getActiveUnitId() : void 0;
1369
+ const unitConfig = {
1370
+ unitScoped,
1371
+ unitColumn: entityDef.data.unitIdColumn ?? registered?.unitColumn,
1372
+ includeSharedRows: entityDef.data.includeSharedRows,
1373
+ unitId
1374
+ };
1314
1375
  const cacheOptions = {
1315
1376
  table: entityDef.data.archetypeKind ? `${entityDef.data.table}#${entityDef.data.archetypeKind}` : entityDef.data.table,
1316
1377
  tenantId,
1317
- ttl: entityDef.data.cacheTTL
1378
+ ttl: entityDef.data.cacheTTL,
1379
+ unitId
1318
1380
  };
1319
1381
  const offlinePolicy = entityDef.data.offline ?? "online";
1320
1382
  const cached = (p) => {
@@ -1327,7 +1389,8 @@ function resolveDataProvider(entityDef, mockData, options = {}) {
1327
1389
  archetypeKind: entityDef.data.archetypeKind,
1328
1390
  projectTable: entityDef.data.table,
1329
1391
  tenantId,
1330
- searchColumns: entityDef.data.searchColumns ?? entityDef.fields.filter((field) => field.searchable).map((field) => field.key)
1392
+ searchColumns: entityDef.data.searchColumns ?? entityDef.fields.filter((field) => field.searchable).map((field) => field.key),
1393
+ ...unitConfig
1331
1394
  }), entityDef.data.archetype));
1332
1395
  }
1333
1396
  return cached(createSupabaseProvider(entityDef.data.table, {
@@ -1338,7 +1401,8 @@ function resolveDataProvider(entityDef, mockData, options = {}) {
1338
1401
  selectColumns: entityDef.data.selectColumns,
1339
1402
  columnMap: entityDef.data.columnMap,
1340
1403
  filters: entityDef.data.filters,
1341
- defaults: entityDef.data.defaults
1404
+ defaults: entityDef.data.defaults,
1405
+ ...unitConfig
1342
1406
  }));
1343
1407
  }
1344
1408
  if (entityDef.data?.table) {
@@ -1349,6 +1413,21 @@ function resolveDataProvider(entityDef, mockData, options = {}) {
1349
1413
  return createMockProvider(entityDef, mockData);
1350
1414
  }
1351
1415
 
1416
+ // src/data/missing-objects.ts
1417
+ var missing = /* @__PURE__ */ new Set();
1418
+ function isKnownMissing(key) {
1419
+ return missing.has(key);
1420
+ }
1421
+ function rememberIfMissing(key, error) {
1422
+ if (!error) return false;
1423
+ if (toDataError(error).kind !== "missing-object") return false;
1424
+ missing.add(key);
1425
+ return true;
1426
+ }
1427
+ function resetMissingObjects() {
1428
+ missing.clear();
1429
+ }
1430
+
1352
1431
  // src/data/query.ts
1353
1432
  var DEFAULT_TIMEOUT_MS = 15e3;
1354
1433
  var DEFAULT_RETRY_DELAY_MS = 150;
@@ -1613,6 +1692,6 @@ async function bulkRemove(provider, ids, options) {
1613
1692
  return { removed, failed };
1614
1693
  }
1615
1694
 
1616
- export { CustomFieldsError, DATA_CHANGED_EVENT, DataError, FIELDS_TABLE, attachPersistentCache, bulkCreate, bulkRemove, cacheKeyFor, cachePrefixFor, clearCustomFieldsCache, countByTenant, createArchetypeProvider, createEventBus, createFayzApiProvider, createMockProvider, dataCall, dataQuery, emitDataChanged, eventBus, getPersistentCache, invalidateCount, localList, matchesDataChange, mergeCustomFields, registerDataTelemetry, replicaKeyFor, resolveCustomFields, resolveDataProvider, syncReplica, toDataError, useDataChanged, validateCustomFields, withCache, withOffline };
1617
- //# sourceMappingURL=chunk-S5FOFKR4.js.map
1618
- //# sourceMappingURL=chunk-S5FOFKR4.js.map
1695
+ export { CustomFieldsError, DATA_CHANGED_EVENT, DataError, FIELDS_TABLE, attachPersistentCache, bulkCreate, bulkRemove, cacheKeyFor, cachePrefixFor, clearCustomFieldsCache, countByTenant, createArchetypeProvider, createEventBus, createFayzApiProvider, createMockProvider, dataCall, dataQuery, emitDataChanged, eventBus, getActiveUnitId, getPersistentCache, invalidateCount, isKnownMissing, isUnitMode, localList, matchesDataChange, mergeCustomFields, registerDataTelemetry, rememberIfMissing, replicaKeyFor, resetActiveUnit, resetMissingObjects, resolveCustomFields, resolveDataProvider, setActiveUnitId, setUnitMode, subscribeActiveUnit, syncReplica, toDataError, useActiveUnitId, useDataChanged, useUnitMode, validateCustomFields, withCache, withOffline };
1696
+ //# sourceMappingURL=chunk-2ZAUJVYP.js.map
1697
+ //# sourceMappingURL=chunk-2ZAUJVYP.js.map