@fayz-ai/saas 0.8.0 → 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.
- package/dist/access/context.d.ts +47 -0
- package/dist/access/context.d.ts.map +1 -0
- package/dist/access/headless.d.ts +21 -0
- package/dist/access/headless.d.ts.map +1 -0
- package/dist/access/index.d.ts +12 -0
- package/dist/access/index.d.ts.map +1 -0
- package/dist/access/limits-registry.d.ts +33 -0
- package/dist/access/limits-registry.d.ts.map +1 -0
- package/dist/access/module-nav.d.ts +15 -0
- package/dist/access/module-nav.d.ts.map +1 -0
- package/dist/access/resolver.d.ts +2 -0
- package/dist/access/resolver.d.ts.map +1 -0
- package/dist/access/types.d.ts +43 -0
- package/dist/access/types.d.ts.map +1 -0
- package/dist/access/upgrade-modal-store.d.ts +23 -0
- package/dist/access/upgrade-modal-store.d.ts.map +1 -0
- package/dist/app/AdminShell.d.ts +3 -0
- package/dist/app/AdminShell.d.ts.map +1 -1
- package/dist/app/admin-app.d.ts +3 -1
- package/dist/app/admin-app.d.ts.map +1 -1
- package/dist/app/config.d.ts +47 -2
- package/dist/app/config.d.ts.map +1 -1
- package/dist/app/core-entities.d.ts +25 -0
- package/dist/app/core-entities.d.ts.map +1 -0
- package/dist/app/derive-contract.d.ts +8 -0
- package/dist/app/derive-contract.d.ts.map +1 -0
- package/dist/app/scaffold.d.ts.map +1 -1
- package/dist/archetype-lookup.d.ts.map +1 -1
- package/dist/billing/index.cjs +6 -2
- package/dist/billing/index.d.ts +2 -0
- package/dist/billing/index.d.ts.map +1 -1
- package/dist/billing/index.js +1 -1
- package/dist/billing/plan-badge.d.ts +17 -0
- package/dist/billing/plan-badge.d.ts.map +1 -0
- package/dist/billing/store.d.ts +6 -1
- package/dist/billing/store.d.ts.map +1 -1
- package/dist/{chunk-3M25GRRU.js → chunk-I3DY47N5.js} +3 -4
- package/dist/chunk-I3DY47N5.js.map +1 -0
- package/dist/chunk-NNFGMUF2.cjs +39 -0
- package/dist/chunk-NNFGMUF2.cjs.map +1 -0
- package/dist/{chunk-4XTQ4V5L.cjs → chunk-SAZLW54Y.cjs} +16 -12
- package/dist/chunk-SAZLW54Y.cjs.map +1 -0
- package/dist/{chunk-IPWCHJPE.cjs → chunk-VKYZACG5.cjs} +5 -6
- package/dist/chunk-VKYZACG5.cjs.map +1 -0
- package/dist/chunk-VN5NTK26.js +36 -0
- package/dist/chunk-VN5NTK26.js.map +1 -0
- package/dist/{chunk-XNWCGJYH.js → chunk-VTEP2AQX.js} +15 -11
- package/dist/chunk-VTEP2AQX.js.map +1 -0
- package/dist/components/shared/ContactPicker.d.ts +92 -0
- package/dist/components/shared/ContactPicker.d.ts.map +1 -0
- package/dist/components/shared/PersonLink.d.ts +12 -1
- package/dist/components/shared/PersonLink.d.ts.map +1 -1
- package/dist/components/shared/create-person.d.ts +20 -0
- package/dist/components/shared/create-person.d.ts.map +1 -0
- package/dist/crud/CrudDetailPage.d.ts.map +1 -1
- package/dist/crud/CrudFormPage.d.ts.map +1 -1
- package/dist/crud/CrudListView.d.ts.map +1 -1
- package/dist/crud/CrudPage.d.ts.map +1 -1
- package/dist/crud/archetypes/AddressesTab.d.ts +21 -0
- package/dist/crud/archetypes/AddressesTab.d.ts.map +1 -0
- package/dist/crud/archetypes/PersonDetailTabs.d.ts +2 -1
- package/dist/crud/archetypes/PersonDetailTabs.d.ts.map +1 -1
- package/dist/crud/createCrudPage.d.ts.map +1 -1
- package/dist/crud/index.d.ts +1 -0
- package/dist/crud/index.d.ts.map +1 -1
- package/dist/index.cjs +4300 -722
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +17 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4080 -542
- package/dist/index.js.map +1 -1
- package/dist/org/index.cjs +11 -11
- package/dist/org/index.js +2 -2
- package/dist/org/store.d.ts.map +1 -1
- package/dist/permissions/context.d.ts +2 -0
- package/dist/permissions/context.d.ts.map +1 -1
- package/dist/permissions/index.cjs +7 -7
- package/dist/permissions/index.js +1 -1
- package/dist/shell/components/auth/ProtectedRoute.d.ts.map +1 -1
- package/dist/shell/components/billing/SoftLimitBanner.d.ts +5 -0
- package/dist/shell/components/billing/SoftLimitBanner.d.ts.map +1 -0
- package/dist/shell/components/billing/SubscriptionPage.d.ts +5 -0
- package/dist/shell/components/billing/SubscriptionPage.d.ts.map +1 -0
- package/dist/shell/components/billing/UpgradeModal.d.ts +3 -0
- package/dist/shell/components/billing/UpgradeModal.d.ts.map +1 -0
- package/dist/shell/components/billing/UpgradePrompt.d.ts +10 -0
- package/dist/shell/components/billing/UpgradePrompt.d.ts.map +1 -0
- package/dist/shell/components/billing/access-contract.d.ts +18 -0
- package/dist/shell/components/billing/access-contract.d.ts.map +1 -0
- package/dist/shell/components/billing/gates.d.ts +14 -0
- package/dist/shell/components/billing/gates.d.ts.map +1 -0
- package/dist/shell/components/billing/index.d.ts +9 -2
- package/dist/shell/components/billing/index.d.ts.map +1 -1
- package/dist/shell/components/chat/ChatFab.d.ts +3 -1
- package/dist/shell/components/chat/ChatFab.d.ts.map +1 -1
- package/dist/shell/components/chat/ChatPanel.d.ts +3 -1
- package/dist/shell/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/shell/components/chat/ConfirmActionCard.d.ts +13 -0
- package/dist/shell/components/chat/ConfirmActionCard.d.ts.map +1 -0
- package/dist/shell/components/chat/markdown.d.ts +5 -0
- package/dist/shell/components/chat/markdown.d.ts.map +1 -0
- package/dist/shell/components/organization/TeamTab.d.ts.map +1 -1
- package/dist/shell/components/organization/TenantOnboarding.d.ts.map +1 -1
- package/dist/shell/components/organization/index.d.ts +0 -1
- package/dist/shell/components/organization/index.d.ts.map +1 -1
- package/dist/shell/components/settings/ConnectedCompanySettings.d.ts.map +1 -1
- package/dist/shell/components/settings/ConnectedFieldRulesSettings.d.ts.map +1 -1
- package/dist/shell/components/settings/ConnectedSecuritySettings.d.ts.map +1 -1
- package/dist/shell/components/settings/LocationsCrudPage.d.ts +1 -1
- package/dist/shell/components/settings/LocationsCrudPage.d.ts.map +1 -1
- package/dist/shell/components/settings/SettingsPage.d.ts +23 -2
- package/dist/shell/components/settings/SettingsPage.d.ts.map +1 -1
- package/dist/shell/hooks/index.d.ts +3 -2
- package/dist/shell/hooks/index.d.ts.map +1 -1
- package/dist/shell/hooks/useAITools.d.ts +2 -0
- package/dist/shell/hooks/useAITools.d.ts.map +1 -1
- package/dist/shell/hooks/useChat.d.ts +22 -0
- package/dist/shell/hooks/useChat.d.ts.map +1 -1
- package/dist/shell/hooks/useSystemPermission.d.ts +3 -0
- package/dist/shell/hooks/useSystemPermission.d.ts.map +1 -0
- package/dist/shell/hooks/useTenantPluginSettings.d.ts +12 -0
- package/dist/shell/hooks/useTenantPluginSettings.d.ts.map +1 -0
- package/dist/shell/lib/ai-tool-handlers.d.ts +119 -0
- package/dist/shell/lib/ai-tool-handlers.d.ts.map +1 -0
- package/dist/shell/lib/core-ai-tools.d.ts +14 -0
- package/dist/shell/lib/core-ai-tools.d.ts.map +1 -1
- package/dist/shell/lib/create-client-orders-provider.d.ts.map +1 -1
- package/dist/shell/lib/fayz-agent.d.ts +23 -0
- package/dist/shell/lib/fayz-agent.d.ts.map +1 -0
- package/dist/shell/lib/i18n.d.ts.map +1 -1
- package/dist/shell/stores/chat.store.d.ts +69 -0
- package/dist/shell/stores/chat.store.d.ts.map +1 -1
- package/dist/shell/types/billing.d.ts +7 -0
- package/dist/shell/types/billing.d.ts.map +1 -1
- package/dist/shell/types/crud.d.ts +1 -1
- package/dist/shell/types/crud.d.ts.map +1 -1
- package/dist/shell/types/plugins.d.ts +2 -45
- package/dist/shell/types/plugins.d.ts.map +1 -1
- package/dist/stores/createCrudStore.d.ts.map +1 -1
- package/package.json +15 -12
- package/src/access/context.tsx +228 -0
- package/src/access/headless.ts +71 -0
- package/src/access/index.ts +27 -0
- package/src/access/limits-registry.ts +86 -0
- package/src/access/module-nav.test.ts +78 -0
- package/src/access/module-nav.ts +82 -0
- package/src/access/resolver.test.ts +143 -0
- package/src/access/resolver.ts +5 -0
- package/src/access/types.ts +47 -0
- package/src/access/upgrade-modal-store.ts +30 -0
- package/src/app/AdminShell.tsx +451 -68
- package/src/app/admin-app.tsx +46 -9
- package/src/app/config.ts +54 -1
- package/src/app/core-entities.ts +110 -0
- package/src/app/derive-contract.test.ts +155 -0
- package/src/app/derive-contract.ts +214 -0
- package/src/app/scaffold.tsx +4 -0
- package/src/archetype-lookup.ts +8 -2
- package/src/billing/index.ts +2 -0
- package/src/billing/plan-badge.ts +32 -0
- package/src/billing/store.ts +8 -1
- package/src/components/shared/ContactPicker.tsx +394 -0
- package/src/components/shared/PersonLink.tsx +11 -3
- package/src/components/shared/create-person.ts +74 -0
- package/src/crud/CrudDetailPage.tsx +8 -1
- package/src/crud/CrudFormPage.tsx +124 -6
- package/src/crud/CrudListView.tsx +16 -1
- package/src/crud/CrudPage.tsx +27 -5
- package/src/crud/DeleteConfirmDialog.tsx +1 -1
- package/src/crud/archetypes/AddressesTab.tsx +387 -0
- package/src/crud/archetypes/PersonDetailTabs.tsx +5 -0
- package/src/crud/createCrudPage.tsx +2 -0
- package/src/crud/index.ts +6 -0
- package/src/env.d.ts +7 -0
- package/src/index.ts +49 -1
- package/src/org/store.ts +9 -1
- package/src/permissions/context.tsx +29 -15
- package/src/shell/components/auth/ProtectedRoute.tsx +12 -2
- package/src/shell/components/billing/SoftLimitBanner.tsx +81 -0
- package/src/shell/components/billing/SubscriptionPage.tsx +372 -0
- package/src/shell/components/billing/UpgradeModal.tsx +102 -0
- package/src/shell/components/billing/UpgradePrompt.tsx +229 -0
- package/src/shell/components/billing/access-contract.ts +64 -0
- package/src/shell/components/billing/gates.tsx +62 -0
- package/src/shell/components/billing/index.ts +18 -2
- package/src/shell/components/chat/ChatFab.tsx +4 -2
- package/src/shell/components/chat/ChatPanel.tsx +174 -23
- package/src/shell/components/chat/ConfirmActionCard.tsx +57 -0
- package/src/shell/components/chat/markdown.tsx +102 -0
- package/src/shell/components/organization/TeamTab.tsx +21 -4
- package/src/shell/components/organization/TenantOnboarding.tsx +3 -1
- package/src/shell/components/organization/index.ts +0 -1
- package/src/shell/components/settings/ConnectedCompanySettings.tsx +4 -1
- package/src/shell/components/settings/ConnectedFieldRulesSettings.tsx +3 -1
- package/src/shell/components/settings/ConnectedSecuritySettings.tsx +3 -1
- package/src/shell/components/settings/LocationsCrudPage.ts +6 -1
- package/src/shell/components/settings/SettingsPage.tsx +63 -21
- package/src/shell/hooks/index.ts +3 -2
- package/src/shell/hooks/useAITools.ts +27 -23
- package/src/shell/hooks/useChat.ts +381 -16
- package/src/shell/hooks/useSystemPermission.ts +23 -0
- package/src/shell/hooks/useTenantPluginSettings.ts +127 -0
- package/src/shell/lib/ai-tool-handlers.ts +910 -0
- package/src/shell/lib/core-ai-tools.ts +234 -2
- package/src/shell/lib/create-client-orders-provider.ts +24 -2
- package/src/shell/lib/fayz-agent.ts +56 -0
- package/src/shell/lib/i18n.ts +71 -0
- package/src/shell/stores/chat.store.ts +118 -1
- package/src/shell/styles.css +30 -0
- package/src/shell/types/billing.ts +8 -0
- package/src/shell/types/crud.ts +1 -1
- package/src/shell/types/plugins.ts +13 -50
- package/src/stores/createCrudStore.ts +9 -0
- package/dist/chunk-3M25GRRU.js.map +0 -1
- package/dist/chunk-4XTQ4V5L.cjs.map +0 -1
- package/dist/chunk-5GYQK5IW.js +0 -21
- package/dist/chunk-5GYQK5IW.js.map +0 -1
- package/dist/chunk-IPWCHJPE.cjs.map +0 -1
- package/dist/chunk-KYID2MR6.cjs +0 -23
- package/dist/chunk-KYID2MR6.cjs.map +0 -1
- package/dist/chunk-XNWCGJYH.js.map +0 -1
- package/dist/shell/components/billing/BillingPage.d.ts +0 -7
- package/dist/shell/components/billing/BillingPage.d.ts.map +0 -1
- package/dist/shell/components/billing/PaywallGate.d.ts +0 -11
- package/dist/shell/components/billing/PaywallGate.d.ts.map +0 -1
- package/dist/shell/components/organization/PermissionGate.d.ts +0 -11
- package/dist/shell/components/organization/PermissionGate.d.ts.map +0 -1
- package/dist/shell/hooks/useBilling.d.ts +0 -14
- package/dist/shell/hooks/useBilling.d.ts.map +0 -1
- package/dist/shell/hooks/usePermission.d.ts +0 -16
- package/dist/shell/hooks/usePermission.d.ts.map +0 -1
- package/src/shell/components/billing/BillingPage.tsx +0 -56
- package/src/shell/components/billing/PaywallGate.tsx +0 -80
- package/src/shell/components/organization/PermissionGate.tsx +0 -16
- package/src/shell/hooks/useBilling.ts +0 -151
- package/src/shell/hooks/usePermission.ts +0 -101
package/src/app/admin-app.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
<
|
|
265
|
-
<
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
{
|
|
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
|
|
274
|
-
|
|
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
|
-
|
|
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
|
+
})
|