@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
@@ -14,6 +14,7 @@ import {
14
14
  type AuthLayout,
15
15
  type AuthPluginOptions,
16
16
  type ResolvedAuthPlugin,
17
+ type LoginAmbassador,
17
18
  } from '@fayz-ai/plugin-auth'
18
19
  import { NavTransitionProvider } from '@fayz-ai/ui'
19
20
  import { createFayzSupabaseClient, getFayzSupabaseClientOptional } from '../supabase/client'
@@ -22,8 +23,10 @@ import { createSupabaseOrgAdapter } from '../org/adapters/supabase'
22
23
  import { createMockOrgAdapter } from '../org/adapters/mock'
23
24
  import { useOrganizationStore } from '../org/store'
24
25
  import { PermissionsProvider } from '../permissions/context'
26
+ import { AccessProvider } from '../access/context'
25
27
  import { ToastProvider } from '../shell/components/notifications/ToastProvider'
26
28
  import { ChatFab, ChatPanel } from '../shell/components/chat'
29
+ import { resolveFayzAgentConnection } from '../shell/lib/fayz-agent'
27
30
  import { useBillingStore } from '../billing/store'
28
31
  import type { Plan } from '@fayz-ai/core'
29
32
  import type { PlanConfig } from '../shell/types/billing'
@@ -58,6 +61,8 @@ function resolveAuthRuntime(config: FayzAppConfig): ResolvedAuthPlugin {
58
61
  logo: auth?.logo ?? auth?.loginLogo,
59
62
  tagline: auth?.tagline ?? auth?.loginTagline,
60
63
  description: auth?.description ?? auth?.loginDescription,
64
+ loginAmbassadors: auth?.loginAmbassadors,
65
+ loginAmbassadorsLabel: auth?.loginAmbassadorsLabel,
61
66
  oauth,
62
67
  routes: auth?.routes,
63
68
  supabase: {
@@ -76,6 +81,8 @@ export function getAuthShellProps(auth: FayzAppConfig['auth']): {
76
81
  loginDescription?: string
77
82
  loginLogo?: React.ReactNode
78
83
  loginLayout?: AuthLayout
84
+ loginAmbassadors?: LoginAmbassador[]
85
+ loginAmbassadorsLabel?: string
79
86
  showOAuth?: boolean
80
87
  oauthProviders?: Exclude<import('@fayz-ai/core').AuthProvider, 'email'>[]
81
88
  } {
@@ -87,6 +94,8 @@ export function getAuthShellProps(auth: FayzAppConfig['auth']): {
87
94
  loginDescription: auth.description,
88
95
  loginLogo: auth.logo,
89
96
  loginLayout: auth.layout,
97
+ loginAmbassadors: auth.loginAmbassadors,
98
+ loginAmbassadorsLabel: auth.loginAmbassadorsLabel,
90
99
  showOAuth: auth.oauth.enabled,
91
100
  oauthProviders: auth.oauth.providers,
92
101
  }
@@ -97,6 +106,8 @@ export function getAuthShellProps(auth: FayzAppConfig['auth']): {
97
106
  loginDescription: auth.description ?? auth.loginDescription,
98
107
  loginLogo: auth.logo ?? auth.loginLogo,
99
108
  loginLayout: auth.layout ?? auth.loginLayout,
109
+ loginAmbassadors: auth.loginAmbassadors,
110
+ loginAmbassadorsLabel: auth.loginAmbassadorsLabel,
100
111
  showOAuth: auth.oauth?.enabled ?? auth.showOAuth,
101
112
  oauthProviders: auth.oauth?.providers ?? auth.oauthProviders,
102
113
  }
@@ -157,9 +168,13 @@ function buildI18nConfig(config: FayzAppConfig): {
157
168
 
158
169
  function BillingInitializer({ config }: { config: FayzAppConfig }) {
159
170
  const setPlans = useBillingStore((s) => s.setPlans)
171
+ const setCheckout = useBillingStore((s) => s.setCheckout)
160
172
  React.useEffect(() => {
161
173
  if (config.billing?.plans) setPlans(config.billing.plans.map(normalizeBillingPlan))
162
- }, [config.billing, setPlans])
174
+ // Seed the payment-gateway seam so the shell's Subscription page can route
175
+ // plan changes through it (Stripe/Pix) instead of a direct updateOrg.
176
+ setCheckout(config.billing?.onCheckout ?? null)
177
+ }, [config.billing, setPlans, setCheckout])
163
178
  return null
164
179
  }
165
180
 
@@ -174,6 +189,10 @@ function normalizeBillingPlan(plan: PlanConfig): Plan {
174
189
  features: plan.features,
175
190
  highlighted: plan.popular,
176
191
  stripePriceId: undefined,
192
+ // Preserve structured entitlements onto the runtime Plan — the access engine
193
+ // reads these to enforce feature gates + quantity caps. (Previously dropped
194
+ // here, leaving plan limits dead.)
195
+ entitlements: plan.entitlements,
177
196
  }
178
197
  }
179
198
 
@@ -210,6 +229,10 @@ function ThemeInitializer({ config }: { config: FayzAppConfig }) {
210
229
 
211
230
  export function AdminProviders({ config, children }: { config: FayzAppConfig; children: React.ReactNode }) {
212
231
  const currentOrg = useOrganizationStore((s) => s.currentOrg)
232
+ const hasFayzAgent = React.useMemo(
233
+ () => !!resolveFayzAgentConnection(config.chat?.agent),
234
+ [config.chat?.agent],
235
+ )
213
236
 
214
237
  // Resolve adapters / i18n once per config identity.
215
238
  const resolved = React.useMemo(() => {
@@ -261,20 +284,34 @@ export function AdminProviders({ config, children }: { config: FayzAppConfig; ch
261
284
  <OrgProvider adapter={resolved.orgAdapter} autoCreate={config.org?.autoCreate ?? true}>
262
285
  <PluginRuntimeProvider value={runtime}>
263
286
  <PermissionsProvider config={config.permissions}>
264
- <I18nProvider value={resolved.i18n}>
265
- <ThemeInitializer config={config} />
266
- <BillingInitializer config={config} />
267
- <ToastProvider />
287
+ <AccessProvider limitDeclarations={config.billing?.limitDeclarations}>
288
+ <I18nProvider value={resolved.i18n}>
289
+ <ThemeInitializer config={config} />
290
+ <BillingInitializer config={config} />
291
+ <ToastProvider />
268
292
  <NavTransitionProvider value={config.navTransition ?? 'slide'}>
269
293
  {children}
270
294
  </NavTransitionProvider>
271
- {config.chat?.enabled !== false && config.chat ? (
295
+ {/* The assistant also renders with no `chat` block at all — a
296
+ project with a Fayz agent enabled injects its connection via
297
+ env, so the app needs no config to light it up. */}
298
+ {config.chat?.enabled !== false && (config.chat || hasFayzAgent) ? (
272
299
  <>
273
- <ChatFab apiEndpoint={config.chat.apiEndpoint} systemPrompt={config.chat.systemPrompt} />
274
- <ChatPanel title={config.chat.title} apiEndpoint={config.chat.apiEndpoint} systemPrompt={config.chat.systemPrompt} />
300
+ <ChatFab
301
+ apiEndpoint={config.chat?.apiEndpoint}
302
+ systemPrompt={config.chat?.systemPrompt}
303
+ agent={config.chat?.agent}
304
+ />
305
+ <ChatPanel
306
+ title={config.chat?.title}
307
+ apiEndpoint={config.chat?.apiEndpoint}
308
+ systemPrompt={config.chat?.systemPrompt}
309
+ agent={config.chat?.agent}
310
+ />
275
311
  </>
276
312
  ) : null}
277
- </I18nProvider>
313
+ </I18nProvider>
314
+ </AccessProvider>
278
315
  </PermissionsProvider>
279
316
  </PluginRuntimeProvider>
280
317
  </OrgProvider>
package/src/app/config.ts CHANGED
@@ -7,12 +7,17 @@ import type {
7
7
  ThemeMode,
8
8
  LocaleConfig,
9
9
  PermissionsConfig,
10
+ LimitDeclaration,
11
+ AgentDomainKnowledge,
12
+ AgentRpcDeclaration,
13
+ EntityDef,
10
14
  } from '@fayz-ai/core'
11
- import type { AuthPluginOptions, ResolvedAuthPlugin } from '@fayz-ai/plugin-auth'
15
+ import type { AuthPluginOptions, ResolvedAuthPlugin, LoginAmbassador } from '@fayz-ai/plugin-auth'
12
16
  import type { BottomNavItem, MobileHeaderVariant } from '@fayz-ai/ui'
13
17
  import type { SaasTheme } from '../shell/config/theme/tokens'
14
18
  import type { CreateThemeOptions } from '../shell/config/theme/utils'
15
19
  import type { PlanConfig } from '../shell/types/billing'
20
+ import type { FayzAgentConnectionConfig } from '../shell/lib/fayz-agent'
16
21
 
17
22
  // ---------------------------------------------------------------------------
18
23
  // Page registration
@@ -63,6 +68,12 @@ export interface AuthConfig {
63
68
  loginLayout?: 'split' | 'centered'
64
69
  loginTagline?: string
65
70
  loginDescription?: string
71
+ /** Product ambassadors shown as a small social-proof avatar strip above the
72
+ * tagline on the split-login brand panel. Each needs a local `image` path;
73
+ * degrades to nothing when omitted. */
74
+ loginAmbassadors?: LoginAmbassador[]
75
+ /** Optional microtext beside the ambassador strip, e.g. "+2 mil profissionais". */
76
+ loginAmbassadorsLabel?: string
66
77
  showOAuth?: boolean
67
78
  oauthProviders?: Exclude<AuthProvider, 'email'>[]
68
79
  }
@@ -81,14 +92,39 @@ export interface OrgConfig {
81
92
  export interface ChatConfig {
82
93
  enabled?: boolean
83
94
  title?: string
95
+ /** Extra guidance appended to the agent's own instructions. */
84
96
  systemPrompt?: string
97
+ /**
98
+ * Bring-your-own chat backend. Leave unset to use the Fayz agent configured
99
+ * for this project — the container injects the connection env, so no key or
100
+ * URL needs to live in app config.
101
+ */
85
102
  apiEndpoint?: string
103
+ /**
104
+ * Overrides for the Fayz agent connection, for apps built outside a Fayz
105
+ * container. `false` opts out of the Fayz agent entirely.
106
+ */
107
+ agent?: FayzAgentConnectionConfig | false
86
108
  }
87
109
 
88
110
  export interface FayzBillingConfig {
89
111
  plans: PlanConfig[]
90
112
  stripePublishableKey?: string
91
113
  portalUrl?: string
114
+ /**
115
+ * Payment-gateway seam. When defined, the shell's Subscription page calls this
116
+ * with the target plan id INSTEAD of writing `plan` straight onto the org — this
117
+ * is where Stripe Checkout / Pix / Mercado Pago plug in (create the intent,
118
+ * redirect, let the webhook persist the plan). Left undefined, changing plan is
119
+ * an optimistic `adapter.updateOrg(orgId, { plan })`.
120
+ */
121
+ onCheckout?: (planId: string) => Promise<void> | void
122
+ /**
123
+ * App-level limit declarations layered on top of the plugins' `declaredLimits`
124
+ * (app wins on key collision). Use to add limits for app-local tables the
125
+ * plugins don't know about, or to re-point a key at a different table.
126
+ */
127
+ limitDeclarations?: LimitDeclaration[]
92
128
  }
93
129
 
94
130
  // ---------------------------------------------------------------------------
@@ -185,4 +221,21 @@ export interface FayzAppConfig {
185
221
  // AI Chat
186
222
  // -------------------------------------------------------------------------
187
223
  chat?: ChatConfig
224
+
225
+ // -------------------------------------------------------------------------
226
+ // Agent contract (manifest v3) — app-level additions to the derived
227
+ // AgentContract: structured domain knowledge for the prompt, app-shipped
228
+ // pool RPCs, and the per-app server-plane flip. Everything else (entities,
229
+ // tools, features, limits) is DERIVED from what the app already declares.
230
+ // -------------------------------------------------------------------------
231
+ agentContract?: {
232
+ /** 'server' flips data tools to broker-side execution for this app. */
233
+ executionPlane?: 'client' | 'server'
234
+ knowledge?: AgentDomainKnowledge
235
+ /** RPCs the app itself ships (vertical-specific, e.g. pricing quote). */
236
+ rpcs?: AgentRpcDeclaration[]
237
+ /** App-level read-models for the agent's data primitives (same contract
238
+ * as PluginManifest.queryEntities — stable ASCII keys). */
239
+ queryEntities?: Array<{ key: string; entity: EntityDef }>
240
+ }
188
241
  }
@@ -0,0 +1,110 @@
1
+ import type { EntityDef } from '@fayz-ai/core'
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // CORE_QUERY_ENTITIES — spine tables the agent may read and write even though
5
+ // no app declares a CRUD page for them.
6
+ //
7
+ // The data primitives (createRecord / updateRecord / searchRecords / queryData)
8
+ // build their entity enum from three sources: the app's registered CRUD
9
+ // entities, plugin registries, and plugin `queryEntities`. A spine table that
10
+ // lives inside a DETAIL TAB rather than on a page of its own belongs to none of
11
+ // them — so the agent could not see it, and would answer "esses campos não
12
+ // estão disponíveis" to a request the UI handles fine.
13
+ //
14
+ // These are declared here, merged in by deriveAgentContract and useAITools, so
15
+ // EVERY app gets them without editing a single app config. They intentionally
16
+ // produce NO route and NO nav item: the surface for an address is the person's
17
+ // Endereços tab, not a top-level page.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /** public.addresses — @fayz-ai/db spine migration 017_core_addresses. */
21
+ const addressEntity: EntityDef = {
22
+ name: 'Address',
23
+ namePlural: 'Addresses',
24
+ icon: 'MapPin',
25
+ data: {
26
+ table: 'addresses',
27
+ tenantScoped: true,
28
+ tenantIdColumn: 'tenant_id',
29
+ searchColumns: ['label', 'street', 'district', 'city', 'postal_code', 'recipient'],
30
+ },
31
+ fields: [
32
+ // owner_id/owner_type are how an address attaches to its person. They are
33
+ // required and described rather than hidden: the agent reaches this table
34
+ // from a record it already has in hand ("adiciona o endereço X pra ele"),
35
+ // so the id it must pass is the one it just searched for or created.
36
+ {
37
+ key: 'owner_id',
38
+ label: 'Owner record id',
39
+ type: 'text',
40
+ required: true,
41
+ hint: 'Id of the person (client, contact, supplier, staff…) this address belongs to.',
42
+ },
43
+ {
44
+ key: 'owner_type',
45
+ label: 'Owner kind',
46
+ type: 'select',
47
+ options: ['person', 'location', 'tenant'],
48
+ hint: "Defaults to 'person'. Only set it for an address that belongs to a business location or to the tenant itself.",
49
+ },
50
+ { key: 'label', label: 'Label', type: 'text', searchable: true, hint: 'Casa, Trabalho, Depósito…' },
51
+ {
52
+ key: 'kind',
53
+ label: 'Use',
54
+ type: 'select',
55
+ options: ['both', 'shipping', 'billing'],
56
+ hint: "Defaults to 'both'.",
57
+ },
58
+ { key: 'postal_code', label: 'Postal code', type: 'text', required: true, searchable: true, hint: 'CEP.' },
59
+ { key: 'street', label: 'Street', type: 'text', required: true, searchable: true },
60
+ { key: 'number', label: 'Number', type: 'text' },
61
+ { key: 'complement', label: 'Complement', type: 'text' },
62
+ { key: 'district', label: 'District', type: 'text', searchable: true, hint: 'Bairro.' },
63
+ { key: 'city', label: 'City', type: 'text', required: true, searchable: true },
64
+ { key: 'state', label: 'State', type: 'text', required: true, hint: 'Two-letter UF.' },
65
+ { key: 'country', label: 'Country', type: 'text', hint: "Defaults to 'BR'." },
66
+ { key: 'recipient', label: 'Recipient', type: 'text', searchable: true, hint: 'Who receives, if not the owner.' },
67
+ { key: 'phone', label: 'Phone', type: 'text' },
68
+ {
69
+ key: 'is_default',
70
+ label: 'Default address',
71
+ type: 'boolean',
72
+ hint: 'Only one default per owner and kind — clear the previous one before setting this.',
73
+ },
74
+ ],
75
+ }
76
+
77
+ export interface CoreQueryEntity {
78
+ key: string
79
+ entity: EntityDef
80
+ writable?: boolean
81
+ /**
82
+ * How this entity attaches to another record — appended to the descriptions
83
+ * of the data primitives that can act on it. These entities do NOT stand
84
+ * alone, and nothing in a flat enum of ~37 keys conveys that.
85
+ *
86
+ * Both directions matter, and each fails differently without it. On WRITE the
87
+ * model reaches for `updateRecord` on the parent, is told the fields do not
88
+ * exist, and then gives up or buries the data in a free-text column. On READ
89
+ * it searches the parent, finds no address on it, and confidently answers
90
+ * "this contact has no address registered" — a falsehood, since the row is
91
+ * one filtered query away.
92
+ *
93
+ * Stated here rather than inferred at failure time on purpose: guessing the
94
+ * intended entity from rejected FIELD NAMES points "add an address to this
95
+ * contact" at the business-locations table, which declares the same columns.
96
+ */
97
+ agentHint?: string
98
+ }
99
+
100
+ export const CORE_QUERY_ENTITIES: CoreQueryEntity[] = [
101
+ {
102
+ key: 'address',
103
+ entity: addressEntity,
104
+ writable: true,
105
+ agentHint:
106
+ '`address` holds the street address of a PERSON (client, contact, supplier, staff). A person record has NO street/city/postal_code fields of its own. ' +
107
+ "To read where someone lives, search `address` with filters {\"owner_id\": <the person's id>} — a person with no address field is not a person without an address. " +
108
+ "To record one, create an `address` with owner_id set to the person's id. Never write an address into a notes field.",
109
+ },
110
+ ]
@@ -0,0 +1,155 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import type { PluginManifest, RegisteredEntity } from '@fayz-ai/core'
3
+ import { deriveAgentContract } from './derive-contract'
4
+ import type { FayzAppConfig } from './config'
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Fixtures — a minimal app: one CRUD entity with a limitKey, one plugin with a
8
+ // persist tool bound to an RPC, a registry, declared features/limits/rpcs.
9
+ // Entities are passed explicitly so the test never touches the global registry.
10
+ // ---------------------------------------------------------------------------
11
+
12
+ const clientEntity: RegisteredEntity = {
13
+ entityKey: 'person:customer',
14
+ label: 'Cliente',
15
+ labelPlural: 'Clientes',
16
+ source: 'app',
17
+ fields: [],
18
+ entityDef: {
19
+ name: 'Cliente',
20
+ namePlural: 'Clientes',
21
+ icon: 'User',
22
+ limitKey: 'clients',
23
+ fields: [
24
+ { key: 'name', label: 'Nome', type: 'text', required: true, searchable: true },
25
+ { key: 'status', label: 'Status', type: 'select', options: [{ label: 'Ativo', value: 'active' }, { label: 'Inativo', value: 'inactive' }] },
26
+ { key: 'origin_id', label: 'Origem', type: 'relation', relation: { table: 'categories' } },
27
+ ],
28
+ data: {
29
+ table: 'clients',
30
+ tenantIdColumn: 'tenant_id',
31
+ archetype: 'person',
32
+ archetypeKind: 'customer',
33
+ searchColumns: ['name', 'email'],
34
+ },
35
+ },
36
+ }
37
+
38
+ const agendaPlugin = {
39
+ id: 'agenda',
40
+ name: 'Agenda',
41
+ version: '1.0.0',
42
+ scope: 'vertical',
43
+ routes: [],
44
+ aiTools: [
45
+ {
46
+ id: 'agenda.create-appointment',
47
+ name: 'createAppointment',
48
+ description: 'Creates a new appointment.',
49
+ mode: 'persist',
50
+ limitKey: 'bookings_month',
51
+ permission: { feature: 'appointments', action: 'create' },
52
+ execution: { plane: 'server', kind: 'rpc', rpc: 'agent_agenda_create_appointment' },
53
+ },
54
+ ],
55
+ declaredFeatures: [{ id: 'appointments', label: 'Appointments' }],
56
+ declaredLimits: [{ key: 'bookings_month', label: 'Appointments/mo', table: 'appointments', period: 'month' }],
57
+ declaredRpcs: [
58
+ { name: 'agent_agenda_create_appointment', kind: 'write', description: 'Guarded appointment create.', audits: true },
59
+ ],
60
+ registries: [
61
+ {
62
+ id: 'cancellation-reasons',
63
+ entity: {
64
+ name: 'Motivo de cancelamento',
65
+ namePlural: 'Motivos de cancelamento',
66
+ icon: 'X',
67
+ fields: [{ key: 'label', label: 'Label', type: 'text' }],
68
+ data: { table: 'appointment_cancellation_reasons' },
69
+ },
70
+ },
71
+ ],
72
+ } as unknown as PluginManifest
73
+
74
+ const config = {
75
+ name: 'Test App',
76
+ plugins: [agendaPlugin],
77
+ permissions: { features: [{ id: 'clients', label: 'Clientes' }] },
78
+ billing: {
79
+ plans: [],
80
+ limitDeclarations: [{ key: 'clients', label: 'Clients (app override)', table: 'clients', kindFilter: 'customer' }],
81
+ },
82
+ chat: { title: 'Assistente Glow', systemPrompt: 'You are the salon assistant.' },
83
+ agentContract: {
84
+ knowledge: { businessRules: [{ id: 'cancel-window', description: 'Cancelations need 2h notice.' }] },
85
+ rpcs: [{ name: 'agent_beauty_quote_service_price', kind: 'read', description: 'Price quote.' }],
86
+ },
87
+ } as unknown as FayzAppConfig
88
+
89
+ describe('deriveAgentContract', () => {
90
+ const { agent, limitDeclarations } = deriveAgentContract(config, [clientEntity])
91
+
92
+ it('projects registered entities with data mapping and fields (no closures)', () => {
93
+ const client = agent.entities?.find((e) => e.key === 'person:customer')
94
+ expect(client).toBeDefined()
95
+ expect(client?.limitKey).toBe('clients')
96
+ expect(client?.data).toMatchObject({ table: 'clients', tenantIdColumn: 'tenant_id', archetypeKind: 'customer' })
97
+ expect(client?.fields.find((f) => f.key === 'status')?.options).toEqual(['active', 'inactive'])
98
+ expect(client?.fields.find((f) => f.key === 'origin_id')?.relationTable).toBe('categories')
99
+ expect(JSON.parse(JSON.stringify(client))).toEqual(client)
100
+ })
101
+
102
+ it('projects plugin registries as entities + registry index', () => {
103
+ expect(agent.registries).toEqual([
104
+ { pluginId: 'agenda', id: 'cancellation-reasons', entityKey: 'agenda:cancellation-reasons' },
105
+ ])
106
+ expect(agent.entities?.some((e) => e.key === 'agenda:cancellation-reasons')).toBe(true)
107
+ })
108
+
109
+ it('derives the two data primitives covering entities + registries', () => {
110
+ const search = agent.tools?.find((t) => t.name === 'searchRecords')
111
+ const query = agent.tools?.find((t) => t.name === 'queryData')
112
+ expect(search?.execution).toEqual({ plane: 'server', kind: 'entity_read', entity: '*' })
113
+ expect(query?.execution).toEqual({ plane: 'server', kind: 'entity_read', entity: '*' })
114
+ // The closed target enum lists both the CRUD entity and the registry key.
115
+ const searchEnum = (search?.parameters?.properties?.entity as { enum?: string[] })?.enum ?? []
116
+ expect(searchEnum).toContain('person:customer')
117
+ expect(searchEnum).toContain('agenda:cancellation-reasons')
118
+ // No per-entity tool remains — the catalog is primitives + declared tools.
119
+ expect(agent.tools?.some((t) => t.id.startsWith('entity.'))).toBe(false)
120
+ })
121
+
122
+ it('keeps plugin-authored execution and defaults persist tools to confirmation', () => {
123
+ const create = agent.tools?.find((t) => t.id === 'agenda.create-appointment')
124
+ expect(create?.execution).toEqual({ plane: 'server', kind: 'rpc', rpc: 'agent_agenda_create_appointment' })
125
+ expect(create?.requiresConfirmation).toBe(true)
126
+ expect(create?.limitKey).toBe('bookings_month')
127
+ expect(create?.pluginId).toBe('agenda')
128
+ })
129
+
130
+ it('marks core tools client-plane', () => {
131
+ const nav = agent.tools?.find((t) => t.name === 'navigateTo')
132
+ expect(nav?.execution).toEqual({ plane: 'client' })
133
+ })
134
+
135
+ it('resolves the 4-layer limit merge with app override winning', () => {
136
+ const clients = limitDeclarations.find((d) => d.key === 'clients')
137
+ expect(clients?.label).toBe('Clients (app override)')
138
+ expect(limitDeclarations.some((d) => d.key === 'users')).toBe(true)
139
+ expect(limitDeclarations.some((d) => d.key === 'bookings_month')).toBe(true)
140
+ })
141
+
142
+ it('collects plugin + app rpcs and app knowledge + persona', () => {
143
+ expect(agent.rpcs?.map((r) => r.name)).toEqual([
144
+ 'agent_agenda_create_appointment',
145
+ 'agent_beauty_quote_service_price',
146
+ ])
147
+ expect(agent.persona).toEqual({ name: 'Assistente Glow', systemPrompt: 'You are the salon assistant.' })
148
+ expect(agent.domainKnowledge?.businessRules?.[0]?.id).toBe('cancel-window')
149
+ expect(agent.declaredFeatures?.map((f) => f.id).sort()).toEqual(['appointments', 'clients'])
150
+ })
151
+
152
+ it('produces a JSON-stable agent section (serializable end to end)', () => {
153
+ expect(JSON.parse(JSON.stringify(agent))).toEqual(agent)
154
+ })
155
+ })