@fayz-ai/saas 0.8.1 → 0.8.2

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 (233) hide show
  1. package/dist/access/context.d.ts +47 -0
  2. package/dist/access/context.d.ts.map +1 -0
  3. package/dist/access/headless.d.ts +21 -0
  4. package/dist/access/headless.d.ts.map +1 -0
  5. package/dist/access/index.d.ts +12 -0
  6. package/dist/access/index.d.ts.map +1 -0
  7. package/dist/access/limits-registry.d.ts +33 -0
  8. package/dist/access/limits-registry.d.ts.map +1 -0
  9. package/dist/access/module-nav.d.ts +15 -0
  10. package/dist/access/module-nav.d.ts.map +1 -0
  11. package/dist/access/resolver.d.ts +2 -0
  12. package/dist/access/resolver.d.ts.map +1 -0
  13. package/dist/access/types.d.ts +43 -0
  14. package/dist/access/types.d.ts.map +1 -0
  15. package/dist/access/upgrade-modal-store.d.ts +23 -0
  16. package/dist/access/upgrade-modal-store.d.ts.map +1 -0
  17. package/dist/app/AdminShell.d.ts +3 -0
  18. package/dist/app/AdminShell.d.ts.map +1 -1
  19. package/dist/app/admin-app.d.ts +3 -1
  20. package/dist/app/admin-app.d.ts.map +1 -1
  21. package/dist/app/config.d.ts +47 -2
  22. package/dist/app/config.d.ts.map +1 -1
  23. package/dist/app/core-entities.d.ts +25 -0
  24. package/dist/app/core-entities.d.ts.map +1 -0
  25. package/dist/app/derive-contract.d.ts +8 -0
  26. package/dist/app/derive-contract.d.ts.map +1 -0
  27. package/dist/app/scaffold.d.ts.map +1 -1
  28. package/dist/archetype-lookup.d.ts.map +1 -1
  29. package/dist/billing/index.cjs +6 -2
  30. package/dist/billing/index.d.ts +2 -0
  31. package/dist/billing/index.d.ts.map +1 -1
  32. package/dist/billing/index.js +1 -1
  33. package/dist/billing/plan-badge.d.ts +17 -0
  34. package/dist/billing/plan-badge.d.ts.map +1 -0
  35. package/dist/billing/store.d.ts +6 -1
  36. package/dist/billing/store.d.ts.map +1 -1
  37. package/dist/{chunk-3M25GRRU.js → chunk-I3DY47N5.js} +3 -4
  38. package/dist/chunk-I3DY47N5.js.map +1 -0
  39. package/dist/chunk-NNFGMUF2.cjs +39 -0
  40. package/dist/chunk-NNFGMUF2.cjs.map +1 -0
  41. package/dist/{chunk-4XTQ4V5L.cjs → chunk-SAZLW54Y.cjs} +16 -12
  42. package/dist/chunk-SAZLW54Y.cjs.map +1 -0
  43. package/dist/{chunk-IPWCHJPE.cjs → chunk-VKYZACG5.cjs} +5 -6
  44. package/dist/chunk-VKYZACG5.cjs.map +1 -0
  45. package/dist/chunk-VN5NTK26.js +36 -0
  46. package/dist/chunk-VN5NTK26.js.map +1 -0
  47. package/dist/{chunk-XNWCGJYH.js → chunk-VTEP2AQX.js} +15 -11
  48. package/dist/chunk-VTEP2AQX.js.map +1 -0
  49. package/dist/components/shared/ContactPicker.d.ts +92 -0
  50. package/dist/components/shared/ContactPicker.d.ts.map +1 -0
  51. package/dist/components/shared/PersonLink.d.ts +12 -1
  52. package/dist/components/shared/PersonLink.d.ts.map +1 -1
  53. package/dist/components/shared/create-person.d.ts +20 -0
  54. package/dist/components/shared/create-person.d.ts.map +1 -0
  55. package/dist/crud/CrudDetailPage.d.ts.map +1 -1
  56. package/dist/crud/CrudFormPage.d.ts.map +1 -1
  57. package/dist/crud/CrudListView.d.ts.map +1 -1
  58. package/dist/crud/CrudPage.d.ts.map +1 -1
  59. package/dist/crud/archetypes/AddressesTab.d.ts +21 -0
  60. package/dist/crud/archetypes/AddressesTab.d.ts.map +1 -0
  61. package/dist/crud/archetypes/PersonDetailTabs.d.ts +2 -1
  62. package/dist/crud/archetypes/PersonDetailTabs.d.ts.map +1 -1
  63. package/dist/crud/createCrudPage.d.ts.map +1 -1
  64. package/dist/crud/index.d.ts +1 -0
  65. package/dist/crud/index.d.ts.map +1 -1
  66. package/dist/index.cjs +4281 -716
  67. package/dist/index.cjs.map +1 -1
  68. package/dist/index.d.ts +17 -2
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4067 -542
  71. package/dist/index.js.map +1 -1
  72. package/dist/org/index.cjs +11 -11
  73. package/dist/org/index.js +2 -2
  74. package/dist/org/store.d.ts.map +1 -1
  75. package/dist/permissions/context.d.ts +2 -0
  76. package/dist/permissions/context.d.ts.map +1 -1
  77. package/dist/permissions/index.cjs +7 -7
  78. package/dist/permissions/index.js +1 -1
  79. package/dist/shell/components/auth/ProtectedRoute.d.ts.map +1 -1
  80. package/dist/shell/components/billing/SoftLimitBanner.d.ts +5 -0
  81. package/dist/shell/components/billing/SoftLimitBanner.d.ts.map +1 -0
  82. package/dist/shell/components/billing/SubscriptionPage.d.ts +5 -0
  83. package/dist/shell/components/billing/SubscriptionPage.d.ts.map +1 -0
  84. package/dist/shell/components/billing/UpgradeModal.d.ts +3 -0
  85. package/dist/shell/components/billing/UpgradeModal.d.ts.map +1 -0
  86. package/dist/shell/components/billing/UpgradePrompt.d.ts +10 -0
  87. package/dist/shell/components/billing/UpgradePrompt.d.ts.map +1 -0
  88. package/dist/shell/components/billing/access-contract.d.ts +18 -0
  89. package/dist/shell/components/billing/access-contract.d.ts.map +1 -0
  90. package/dist/shell/components/billing/gates.d.ts +14 -0
  91. package/dist/shell/components/billing/gates.d.ts.map +1 -0
  92. package/dist/shell/components/billing/index.d.ts +9 -2
  93. package/dist/shell/components/billing/index.d.ts.map +1 -1
  94. package/dist/shell/components/chat/ChatFab.d.ts +3 -1
  95. package/dist/shell/components/chat/ChatFab.d.ts.map +1 -1
  96. package/dist/shell/components/chat/ChatPanel.d.ts +3 -1
  97. package/dist/shell/components/chat/ChatPanel.d.ts.map +1 -1
  98. package/dist/shell/components/chat/ConfirmActionCard.d.ts +13 -0
  99. package/dist/shell/components/chat/ConfirmActionCard.d.ts.map +1 -0
  100. package/dist/shell/components/chat/markdown.d.ts +5 -0
  101. package/dist/shell/components/chat/markdown.d.ts.map +1 -0
  102. package/dist/shell/components/organization/TeamTab.d.ts.map +1 -1
  103. package/dist/shell/components/organization/TenantOnboarding.d.ts.map +1 -1
  104. package/dist/shell/components/organization/index.d.ts +0 -1
  105. package/dist/shell/components/organization/index.d.ts.map +1 -1
  106. package/dist/shell/components/settings/ConnectedCompanySettings.d.ts.map +1 -1
  107. package/dist/shell/components/settings/ConnectedFieldRulesSettings.d.ts.map +1 -1
  108. package/dist/shell/components/settings/ConnectedSecuritySettings.d.ts.map +1 -1
  109. package/dist/shell/components/settings/LocationsCrudPage.d.ts +1 -1
  110. package/dist/shell/components/settings/LocationsCrudPage.d.ts.map +1 -1
  111. package/dist/shell/components/settings/SettingsPage.d.ts +23 -2
  112. package/dist/shell/components/settings/SettingsPage.d.ts.map +1 -1
  113. package/dist/shell/hooks/index.d.ts +3 -2
  114. package/dist/shell/hooks/index.d.ts.map +1 -1
  115. package/dist/shell/hooks/useAITools.d.ts +2 -0
  116. package/dist/shell/hooks/useAITools.d.ts.map +1 -1
  117. package/dist/shell/hooks/useChat.d.ts +22 -0
  118. package/dist/shell/hooks/useChat.d.ts.map +1 -1
  119. package/dist/shell/hooks/useSystemPermission.d.ts +3 -0
  120. package/dist/shell/hooks/useSystemPermission.d.ts.map +1 -0
  121. package/dist/shell/hooks/useTenantPluginSettings.d.ts +12 -0
  122. package/dist/shell/hooks/useTenantPluginSettings.d.ts.map +1 -0
  123. package/dist/shell/lib/ai-tool-handlers.d.ts +119 -0
  124. package/dist/shell/lib/ai-tool-handlers.d.ts.map +1 -0
  125. package/dist/shell/lib/core-ai-tools.d.ts +14 -0
  126. package/dist/shell/lib/core-ai-tools.d.ts.map +1 -1
  127. package/dist/shell/lib/create-client-orders-provider.d.ts.map +1 -1
  128. package/dist/shell/lib/fayz-agent.d.ts +23 -0
  129. package/dist/shell/lib/fayz-agent.d.ts.map +1 -0
  130. package/dist/shell/lib/i18n.d.ts.map +1 -1
  131. package/dist/shell/stores/chat.store.d.ts +69 -0
  132. package/dist/shell/stores/chat.store.d.ts.map +1 -1
  133. package/dist/shell/types/billing.d.ts +7 -0
  134. package/dist/shell/types/billing.d.ts.map +1 -1
  135. package/dist/shell/types/plugins.d.ts +2 -45
  136. package/dist/shell/types/plugins.d.ts.map +1 -1
  137. package/dist/stores/createCrudStore.d.ts.map +1 -1
  138. package/package.json +14 -11
  139. package/src/access/context.tsx +228 -0
  140. package/src/access/headless.ts +71 -0
  141. package/src/access/index.ts +27 -0
  142. package/src/access/limits-registry.ts +86 -0
  143. package/src/access/module-nav.test.ts +78 -0
  144. package/src/access/module-nav.ts +82 -0
  145. package/src/access/resolver.test.ts +143 -0
  146. package/src/access/resolver.ts +5 -0
  147. package/src/access/types.ts +47 -0
  148. package/src/access/upgrade-modal-store.ts +30 -0
  149. package/src/app/AdminShell.tsx +451 -68
  150. package/src/app/admin-app.tsx +46 -9
  151. package/src/app/config.ts +54 -1
  152. package/src/app/core-entities.ts +110 -0
  153. package/src/app/derive-contract.test.ts +155 -0
  154. package/src/app/derive-contract.ts +214 -0
  155. package/src/app/scaffold.tsx +4 -0
  156. package/src/archetype-lookup.ts +8 -2
  157. package/src/billing/index.ts +2 -0
  158. package/src/billing/plan-badge.ts +32 -0
  159. package/src/billing/store.ts +8 -1
  160. package/src/components/shared/ContactPicker.tsx +394 -0
  161. package/src/components/shared/PersonLink.tsx +11 -3
  162. package/src/components/shared/create-person.ts +74 -0
  163. package/src/crud/CrudDetailPage.tsx +5 -1
  164. package/src/crud/CrudFormPage.tsx +113 -6
  165. package/src/crud/CrudListView.tsx +16 -1
  166. package/src/crud/CrudPage.tsx +27 -5
  167. package/src/crud/DeleteConfirmDialog.tsx +1 -1
  168. package/src/crud/archetypes/AddressesTab.tsx +387 -0
  169. package/src/crud/archetypes/PersonDetailTabs.tsx +5 -0
  170. package/src/crud/createCrudPage.tsx +2 -0
  171. package/src/crud/index.ts +6 -0
  172. package/src/env.d.ts +7 -0
  173. package/src/index.ts +49 -1
  174. package/src/org/store.ts +9 -1
  175. package/src/permissions/context.tsx +29 -15
  176. package/src/shell/components/auth/ProtectedRoute.tsx +12 -2
  177. package/src/shell/components/billing/SoftLimitBanner.tsx +81 -0
  178. package/src/shell/components/billing/SubscriptionPage.tsx +372 -0
  179. package/src/shell/components/billing/UpgradeModal.tsx +102 -0
  180. package/src/shell/components/billing/UpgradePrompt.tsx +229 -0
  181. package/src/shell/components/billing/access-contract.ts +64 -0
  182. package/src/shell/components/billing/gates.tsx +62 -0
  183. package/src/shell/components/billing/index.ts +18 -2
  184. package/src/shell/components/chat/ChatFab.tsx +4 -2
  185. package/src/shell/components/chat/ChatPanel.tsx +174 -23
  186. package/src/shell/components/chat/ConfirmActionCard.tsx +57 -0
  187. package/src/shell/components/chat/markdown.tsx +102 -0
  188. package/src/shell/components/organization/TeamTab.tsx +21 -4
  189. package/src/shell/components/organization/TenantOnboarding.tsx +3 -1
  190. package/src/shell/components/organization/index.ts +0 -1
  191. package/src/shell/components/settings/ConnectedCompanySettings.tsx +4 -1
  192. package/src/shell/components/settings/ConnectedFieldRulesSettings.tsx +3 -1
  193. package/src/shell/components/settings/ConnectedSecuritySettings.tsx +3 -1
  194. package/src/shell/components/settings/LocationsCrudPage.ts +6 -1
  195. package/src/shell/components/settings/SettingsPage.tsx +63 -21
  196. package/src/shell/hooks/index.ts +3 -2
  197. package/src/shell/hooks/useAITools.ts +27 -23
  198. package/src/shell/hooks/useChat.ts +381 -16
  199. package/src/shell/hooks/useSystemPermission.ts +23 -0
  200. package/src/shell/hooks/useTenantPluginSettings.ts +127 -0
  201. package/src/shell/lib/ai-tool-handlers.ts +910 -0
  202. package/src/shell/lib/core-ai-tools.ts +234 -2
  203. package/src/shell/lib/create-client-orders-provider.ts +24 -2
  204. package/src/shell/lib/fayz-agent.ts +56 -0
  205. package/src/shell/lib/i18n.ts +71 -0
  206. package/src/shell/stores/chat.store.ts +118 -1
  207. package/src/shell/styles.css +30 -0
  208. package/src/shell/types/billing.ts +8 -0
  209. package/src/shell/types/plugins.ts +13 -50
  210. package/src/stores/createCrudStore.ts +9 -0
  211. package/dist/chunk-3M25GRRU.js.map +0 -1
  212. package/dist/chunk-4XTQ4V5L.cjs.map +0 -1
  213. package/dist/chunk-5GYQK5IW.js +0 -21
  214. package/dist/chunk-5GYQK5IW.js.map +0 -1
  215. package/dist/chunk-IPWCHJPE.cjs.map +0 -1
  216. package/dist/chunk-KYID2MR6.cjs +0 -23
  217. package/dist/chunk-KYID2MR6.cjs.map +0 -1
  218. package/dist/chunk-XNWCGJYH.js.map +0 -1
  219. package/dist/shell/components/billing/BillingPage.d.ts +0 -7
  220. package/dist/shell/components/billing/BillingPage.d.ts.map +0 -1
  221. package/dist/shell/components/billing/PaywallGate.d.ts +0 -11
  222. package/dist/shell/components/billing/PaywallGate.d.ts.map +0 -1
  223. package/dist/shell/components/organization/PermissionGate.d.ts +0 -11
  224. package/dist/shell/components/organization/PermissionGate.d.ts.map +0 -1
  225. package/dist/shell/hooks/useBilling.d.ts +0 -14
  226. package/dist/shell/hooks/useBilling.d.ts.map +0 -1
  227. package/dist/shell/hooks/usePermission.d.ts +0 -16
  228. package/dist/shell/hooks/usePermission.d.ts.map +0 -1
  229. package/src/shell/components/billing/BillingPage.tsx +0 -56
  230. package/src/shell/components/billing/PaywallGate.tsx +0 -80
  231. package/src/shell/components/organization/PermissionGate.tsx +0 -16
  232. package/src/shell/hooks/useBilling.ts +0 -151
  233. package/src/shell/hooks/usePermission.ts +0 -101
@@ -0,0 +1,143 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import type { PermissionProfile, Plan } from '@fayz-ai/core'
3
+ import { resolveAccess, isEntitledByPlan, resolveLimit } from './resolver'
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Fixtures
7
+ // ---------------------------------------------------------------------------
8
+
9
+ const ownerProfile: PermissionProfile = { id: 'owner', name: 'Owner', grants: {} }
10
+
11
+ const staffProfile: PermissionProfile = {
12
+ id: 'staff',
13
+ name: 'Staff',
14
+ grants: { clients: ['read'], reports: ['read', 'edit'] },
15
+ }
16
+
17
+ function plan(entitlements: Plan['entitlements']): Plan {
18
+ return {
19
+ id: 'p',
20
+ name: 'Plan',
21
+ price: 0,
22
+ currency: 'USD',
23
+ interval: 'month',
24
+ features: [],
25
+ entitlements,
26
+ }
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // resolveAccess — role × plan composition
31
+ // ---------------------------------------------------------------------------
32
+
33
+ describe('resolveAccess', () => {
34
+ it('denies by ROLE when the profile lacks the grant (non-owner)', () => {
35
+ const d = resolveAccess({ profile: staffProfile, plan: null }, 'marketing')
36
+ expect(d).toEqual({ allowed: false, reason: 'role' })
37
+ })
38
+
39
+ it('role denial takes precedence over plan denial', () => {
40
+ // staff lacks 'marketing' (role) AND the plan gates it off (plan). Role wins.
41
+ const p = plan({ features: { marketing: false } })
42
+ const d = resolveAccess({ profile: staffProfile, plan: p }, 'marketing')
43
+ expect(d.reason).toBe('role')
44
+ })
45
+
46
+ it('allows when role grants and the plan does not gate the feature', () => {
47
+ const d = resolveAccess({ profile: staffProfile, plan: plan({}) }, 'clients')
48
+ expect(d).toEqual({ allowed: true })
49
+ })
50
+
51
+ it('denies by PLAN when role grants but the plan gates the feature off', () => {
52
+ const p = plan({ features: { reports: false } })
53
+ const d = resolveAccess({ profile: staffProfile, plan: p }, 'reports')
54
+ expect(d).toEqual({ allowed: false, reason: 'plan' })
55
+ })
56
+
57
+ it('OWNER passes the role check but does NOT bypass the plan', () => {
58
+ const p = plan({ features: { reports: false } })
59
+ const d = resolveAccess({ profile: ownerProfile, plan: p }, 'reports')
60
+ expect(d).toEqual({ allowed: false, reason: 'plan' })
61
+ })
62
+
63
+ it('owner is allowed when the plan does not gate the feature', () => {
64
+ const d = resolveAccess({ profile: ownerProfile, plan: plan({}) }, 'anything')
65
+ expect(d).toEqual({ allowed: true })
66
+ })
67
+
68
+ it('a feature absent from entitlements is allowed (additive plans)', () => {
69
+ const p = plan({ features: { other: false } })
70
+ const d = resolveAccess({ profile: ownerProfile, plan: p }, 'prontuario')
71
+ expect(d).toEqual({ allowed: true })
72
+ })
73
+
74
+ it('respects the action argument (manage/absent)', () => {
75
+ // staff has clients:['read'] only → write denied by role.
76
+ expect(resolveAccess({ profile: staffProfile, plan: null }, 'clients', 'write').reason).toBe('role')
77
+ expect(resolveAccess({ profile: staffProfile, plan: null }, 'clients', 'read').allowed).toBe(true)
78
+ })
79
+
80
+ it('impersonation preview composes: previewing a restricted role loses owner bypass', () => {
81
+ // During impersonation the session.profile IS the previewed (staff) role.
82
+ const d = resolveAccess({ profile: staffProfile, plan: plan({}) }, 'billing')
83
+ expect(d).toEqual({ allowed: false, reason: 'role' })
84
+ })
85
+ })
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // isEntitledByPlan
89
+ // ---------------------------------------------------------------------------
90
+
91
+ describe('isEntitledByPlan', () => {
92
+ it('null plan is entitled (no gating)', () => {
93
+ expect(isEntitledByPlan(null, 'reports')).toBe(true)
94
+ })
95
+ it('explicit false denies', () => {
96
+ expect(isEntitledByPlan(plan({ features: { reports: false } }), 'reports')).toBe(false)
97
+ })
98
+ it('explicit true and absent both allow', () => {
99
+ expect(isEntitledByPlan(plan({ features: { reports: true } }), 'reports')).toBe(true)
100
+ expect(isEntitledByPlan(plan({}), 'reports')).toBe(true)
101
+ })
102
+ })
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // resolveLimit — quantity math
106
+ // ---------------------------------------------------------------------------
107
+
108
+ describe('resolveLimit', () => {
109
+ it('unlimited when there is no declaration', () => {
110
+ const s = resolveLimit({ cap: 100, hasDeclaration: false, used: 50 })
111
+ expect(s.unlimited).toBe(true)
112
+ expect(s.max).toBe(Infinity)
113
+ expect(s.atLimit).toBe(false)
114
+ })
115
+
116
+ it('unlimited when the plan has no cap for the key', () => {
117
+ const s = resolveLimit({ cap: undefined, hasDeclaration: true, used: 999 })
118
+ expect(s.unlimited).toBe(true)
119
+ })
120
+
121
+ it('-1 means unlimited', () => {
122
+ const s = resolveLimit({ cap: -1, hasDeclaration: true, used: 999 })
123
+ expect(s.unlimited).toBe(true)
124
+ expect(s.remaining).toBe(Infinity)
125
+ })
126
+
127
+ it('computes remaining and not-at-limit below the cap', () => {
128
+ const s = resolveLimit({ cap: 100, hasDeclaration: true, used: 40 })
129
+ expect(s).toMatchObject({ unlimited: false, max: 100, remaining: 60, atLimit: false })
130
+ })
131
+
132
+ it('atLimit when used reaches the cap', () => {
133
+ const s = resolveLimit({ cap: 100, hasDeclaration: true, used: 100 })
134
+ expect(s.atLimit).toBe(true)
135
+ expect(s.remaining).toBe(0)
136
+ })
137
+
138
+ it('clamps remaining at 0 when over the cap', () => {
139
+ const s = resolveLimit({ cap: 2, hasDeclaration: true, used: 5 })
140
+ expect(s.remaining).toBe(0)
141
+ expect(s.atLimit).toBe(true)
142
+ })
143
+ })
@@ -0,0 +1,5 @@
1
+ // The decision engine moved to @fayz-ai/core/access so the SAME implementation
2
+ // runs in the browser, in headless client-plane code and in the Fayz broker
3
+ // (server-side agent authorization). Re-exported here so existing call-sites
4
+ // keep importing from saas unchanged.
5
+ export { isEntitledByPlan, resolveAccess, resolveLimit } from '@fayz-ai/core/access'
@@ -0,0 +1,47 @@
1
+ import type { AuthUser, Organization, PermissionProfile, Plan, PermissionAction } from '@fayz-ai/core'
2
+
3
+ /**
4
+ * Decision types come from the shared engine in @fayz-ai/core/access.
5
+ * - `role` — the user's RBAC profile lacks the permission (fix: grant the role).
6
+ * - `plan` — the tenant's plan does not entitle the feature (fix: upgrade).
7
+ * - `limit` — a guarded write would exceed a plan cap (fix: upgrade).
8
+ * The distinction drives the UI: `role` → AccessDenied, `plan`/`limit` →
9
+ * UpgradePrompt/UpgradeModal.
10
+ */
11
+ import type { AccessDecision } from '@fayz-ai/core/access'
12
+
13
+ export type { DenyReason, AccessDecision } from '@fayz-ai/core/access'
14
+
15
+ /**
16
+ * The reactive snapshot the access engine resolves against. Assembled from the
17
+ * singleton stores (auth user, current org incl. plan id, current permission
18
+ * profile, billing plans). This is intentionally ONE extensible object: future
19
+ * session parameters (active location, feature-flag cohort, delegated scope, …)
20
+ * enter here so the resolver signature never churns.
21
+ */
22
+ export interface AccessSession {
23
+ user: AuthUser | null
24
+ org: Organization | null
25
+ /** Effective profile — becomes the previewed role during impersonation. */
26
+ profile: PermissionProfile | null
27
+ /** The org's currently-active plan (resolved from billing plans by org.plan). */
28
+ plan: Plan | null
29
+ }
30
+
31
+ export interface LimitState {
32
+ key: string
33
+ /** Cap from the plan (`-1`/unlimited surfaces as `Infinity` here). */
34
+ max: number
35
+ used: number
36
+ remaining: number
37
+ atLimit: boolean
38
+ unlimited: boolean
39
+ loading: boolean
40
+ /** Re-count (bypasses the short-lived cache). */
41
+ refresh(): void
42
+ }
43
+
44
+ export interface AccessApi {
45
+ can(feature: string, action?: PermissionAction): AccessDecision
46
+ entitled(feature: string): boolean
47
+ }
@@ -0,0 +1,30 @@
1
+ import { create } from 'zustand'
2
+
3
+ /**
4
+ * Payload describing WHY the upgrade modal opened, so the visual component (owned
5
+ * by the shell/gates layer) can tailor its copy — a hit quantity cap
6
+ * (`limitKey`) or a plan-gated feature (`feature`).
7
+ */
8
+ export interface UpgradeModalPayload {
9
+ limitKey?: string
10
+ feature?: string
11
+ }
12
+
13
+ export interface UpgradeModalStore {
14
+ /** The active payload, or null when closed. */
15
+ current: UpgradeModalPayload | null
16
+ open(payload: UpgradeModalPayload): void
17
+ close(): void
18
+ }
19
+
20
+ /**
21
+ * Global, lightweight store for the single app-wide UpgradeModal. `useLimitGuard`
22
+ * and imperative create handlers call `open(...)` when a limit blocks; the visual
23
+ * modal (rendered once near the shell root by the gates layer) subscribes to
24
+ * `current`. Kept as a zustand singleton so store + component share one instance.
25
+ */
26
+ export const useUpgradeModalStore = create<UpgradeModalStore>((set) => ({
27
+ current: null,
28
+ open: (payload) => set({ current: payload }),
29
+ close: () => set({ current: null }),
30
+ }))