@fayz-ai/core 0.1.6

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 (172) hide show
  1. package/dist/app/render.d.ts +16 -0
  2. package/dist/app/render.d.ts.map +1 -0
  3. package/dist/blocks/index.d.ts +25 -0
  4. package/dist/blocks/index.d.ts.map +1 -0
  5. package/dist/chunk-2NTHNWEH.cjs +32 -0
  6. package/dist/chunk-2NTHNWEH.cjs.map +1 -0
  7. package/dist/chunk-5EAZOMVP.js +1609 -0
  8. package/dist/chunk-5EAZOMVP.js.map +1 -0
  9. package/dist/chunk-5TKIZVVJ.cjs +16 -0
  10. package/dist/chunk-5TKIZVVJ.cjs.map +1 -0
  11. package/dist/chunk-7RTUHSKY.js +3 -0
  12. package/dist/chunk-7RTUHSKY.js.map +1 -0
  13. package/dist/chunk-AXTWKF5X.cjs +38 -0
  14. package/dist/chunk-AXTWKF5X.cjs.map +1 -0
  15. package/dist/chunk-EGWWQ7AT.cjs +355 -0
  16. package/dist/chunk-EGWWQ7AT.cjs.map +1 -0
  17. package/dist/chunk-KAYQFSR6.js +298 -0
  18. package/dist/chunk-KAYQFSR6.js.map +1 -0
  19. package/dist/chunk-LSVBDYFQ.cjs +1639 -0
  20. package/dist/chunk-LSVBDYFQ.cjs.map +1 -0
  21. package/dist/chunk-SNQW6TRF.cjs +658 -0
  22. package/dist/chunk-SNQW6TRF.cjs.map +1 -0
  23. package/dist/chunk-WKZVSKEU.js +26 -0
  24. package/dist/chunk-WKZVSKEU.js.map +1 -0
  25. package/dist/chunk-Y2C2FBQP.js +35 -0
  26. package/dist/chunk-Y2C2FBQP.js.map +1 -0
  27. package/dist/chunk-YSBXGHAM.js +643 -0
  28. package/dist/chunk-YSBXGHAM.js.map +1 -0
  29. package/dist/data/archetype.d.ts +13 -0
  30. package/dist/data/archetype.d.ts.map +1 -0
  31. package/dist/data/cached.d.ts +19 -0
  32. package/dist/data/cached.d.ts.map +1 -0
  33. package/dist/data/fayz-api.d.ts +19 -0
  34. package/dist/data/fayz-api.d.ts.map +1 -0
  35. package/dist/data/index.cjs +32 -0
  36. package/dist/data/index.cjs.map +1 -0
  37. package/dist/data/index.d.ts +9 -0
  38. package/dist/data/index.d.ts.map +1 -0
  39. package/dist/data/index.js +3 -0
  40. package/dist/data/index.js.map +1 -0
  41. package/dist/data/mock.d.ts +7 -0
  42. package/dist/data/mock.d.ts.map +1 -0
  43. package/dist/data/resolve.d.ts +22 -0
  44. package/dist/data/resolve.d.ts.map +1 -0
  45. package/dist/data/supabase.d.ts +18 -0
  46. package/dist/data/supabase.d.ts.map +1 -0
  47. package/dist/data/types.d.ts +23 -0
  48. package/dist/data/types.d.ts.map +1 -0
  49. package/dist/entity/index.cjs +28 -0
  50. package/dist/entity/index.cjs.map +1 -0
  51. package/dist/entity/index.d.ts +3 -0
  52. package/dist/entity/index.d.ts.map +1 -0
  53. package/dist/entity/index.js +3 -0
  54. package/dist/entity/index.js.map +1 -0
  55. package/dist/entity/registry.d.ts +20 -0
  56. package/dist/entity/registry.d.ts.map +1 -0
  57. package/dist/events/index.d.ts +16 -0
  58. package/dist/events/index.d.ts.map +1 -0
  59. package/dist/i18n/index.cjs +44 -0
  60. package/dist/i18n/index.cjs.map +1 -0
  61. package/dist/i18n/index.d.ts +21 -0
  62. package/dist/i18n/index.d.ts.map +1 -0
  63. package/dist/i18n/index.js +3 -0
  64. package/dist/i18n/index.js.map +1 -0
  65. package/dist/i18n/shell-translations.d.ts +2 -0
  66. package/dist/i18n/shell-translations.d.ts.map +1 -0
  67. package/dist/index.cjs +911 -0
  68. package/dist/index.cjs.map +1 -0
  69. package/dist/index.d.ts +46 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +596 -0
  72. package/dist/index.js.map +1 -0
  73. package/dist/lib/cache.d.ts +21 -0
  74. package/dist/lib/cache.d.ts.map +1 -0
  75. package/dist/lib/csv.d.ts +14 -0
  76. package/dist/lib/csv.d.ts.map +1 -0
  77. package/dist/lib/format.d.ts +7 -0
  78. package/dist/lib/format.d.ts.map +1 -0
  79. package/dist/manifest/index.d.ts +80 -0
  80. package/dist/manifest/index.d.ts.map +1 -0
  81. package/dist/plugin/createSafeDataProvider.d.ts +14 -0
  82. package/dist/plugin/createSafeDataProvider.d.ts.map +1 -0
  83. package/dist/plugin/index.cjs +32 -0
  84. package/dist/plugin/index.cjs.map +1 -0
  85. package/dist/plugin/index.d.ts +2 -0
  86. package/dist/plugin/index.d.ts.map +1 -0
  87. package/dist/plugin/index.js +3 -0
  88. package/dist/plugin/index.js.map +1 -0
  89. package/dist/plugin/runtime.d.ts +31 -0
  90. package/dist/plugin/runtime.d.ts.map +1 -0
  91. package/dist/registry/index.d.ts +88 -0
  92. package/dist/registry/index.d.ts.map +1 -0
  93. package/dist/router/index.cjs +16 -0
  94. package/dist/router/index.cjs.map +1 -0
  95. package/dist/router/index.d.ts +8 -0
  96. package/dist/router/index.d.ts.map +1 -0
  97. package/dist/router/index.js +3 -0
  98. package/dist/router/index.js.map +1 -0
  99. package/dist/runtime/index.cjs +16 -0
  100. package/dist/runtime/index.cjs.map +1 -0
  101. package/dist/runtime/index.d.ts +3 -0
  102. package/dist/runtime/index.d.ts.map +1 -0
  103. package/dist/runtime/index.js +3 -0
  104. package/dist/runtime/index.js.map +1 -0
  105. package/dist/runtime/oauth.d.ts +96 -0
  106. package/dist/runtime/oauth.d.ts.map +1 -0
  107. package/dist/tenant/index.d.ts +3 -0
  108. package/dist/tenant/index.d.ts.map +1 -0
  109. package/dist/types/auth.d.ts +27 -0
  110. package/dist/types/auth.d.ts.map +1 -0
  111. package/dist/types/billing.d.ts +17 -0
  112. package/dist/types/billing.d.ts.map +1 -0
  113. package/dist/types/crud.d.ts +107 -0
  114. package/dist/types/crud.d.ts.map +1 -0
  115. package/dist/types/entities.d.ts +134 -0
  116. package/dist/types/entities.d.ts.map +1 -0
  117. package/dist/types/i18n.d.ts +8 -0
  118. package/dist/types/i18n.d.ts.map +1 -0
  119. package/dist/types/index.cjs +4 -0
  120. package/dist/types/index.cjs.map +1 -0
  121. package/dist/types/index.d.ts +10 -0
  122. package/dist/types/index.d.ts.map +1 -0
  123. package/dist/types/index.js +3 -0
  124. package/dist/types/index.js.map +1 -0
  125. package/dist/types/org.d.ts +96 -0
  126. package/dist/types/org.d.ts.map +1 -0
  127. package/dist/types/permissions.d.ts +25 -0
  128. package/dist/types/permissions.d.ts.map +1 -0
  129. package/dist/types/plugins.d.ts +314 -0
  130. package/dist/types/plugins.d.ts.map +1 -0
  131. package/dist/types/theme.d.ts +16 -0
  132. package/dist/types/theme.d.ts.map +1 -0
  133. package/package.json +98 -0
  134. package/src/app/render.tsx +69 -0
  135. package/src/blocks/index.tsx +68 -0
  136. package/src/data/archetype.ts +210 -0
  137. package/src/data/cached.ts +55 -0
  138. package/src/data/fayz-api.ts +200 -0
  139. package/src/data/index.ts +8 -0
  140. package/src/data/mock.ts +84 -0
  141. package/src/data/resolve.ts +108 -0
  142. package/src/data/supabase.ts +169 -0
  143. package/src/data/types.ts +20 -0
  144. package/src/entity/index.ts +2 -0
  145. package/src/entity/registry.ts +42 -0
  146. package/src/events/index.tsx +82 -0
  147. package/src/i18n/index.ts +250 -0
  148. package/src/i18n/shell-translations.ts +1391 -0
  149. package/src/index.ts +132 -0
  150. package/src/lib/cache.ts +118 -0
  151. package/src/lib/csv.ts +54 -0
  152. package/src/lib/format.ts +58 -0
  153. package/src/manifest/app-manifest.schema.json +103 -0
  154. package/src/manifest/index.ts +394 -0
  155. package/src/plugin/createSafeDataProvider.ts +30 -0
  156. package/src/plugin/index.ts +1 -0
  157. package/src/plugin/runtime.ts +313 -0
  158. package/src/registry/index.ts +163 -0
  159. package/src/router/index.ts +29 -0
  160. package/src/runtime/index.ts +15 -0
  161. package/src/runtime/oauth.ts +219 -0
  162. package/src/tenant/index.ts +18 -0
  163. package/src/types/auth.ts +26 -0
  164. package/src/types/billing.ts +17 -0
  165. package/src/types/crud.ts +108 -0
  166. package/src/types/entities.ts +142 -0
  167. package/src/types/i18n.ts +7 -0
  168. package/src/types/index.ts +10 -0
  169. package/src/types/org.ts +94 -0
  170. package/src/types/permissions.ts +32 -0
  171. package/src/types/plugins.ts +363 -0
  172. package/src/types/theme.ts +17 -0
@@ -0,0 +1,313 @@
1
+ import * as React from 'react'
2
+ import type {
3
+ PluginManifest,
4
+ PluginRuntime,
5
+ PluginRuntimeContext,
6
+ PluginRuntimeIssue,
7
+ PluginRuntimeRoute,
8
+ PluginSettingsTab,
9
+ PluginWidgetDefinition,
10
+ PluginWidgetZone,
11
+ ResolvedPluginManifest,
12
+ ResolvedPluginWidget,
13
+ TenantPluginBinding,
14
+ PluginRegistryDef,
15
+ PluginAITool,
16
+ PluginNavigationEntry,
17
+ PluginCapability,
18
+ PluginWidgetVisibility,
19
+ ResolvedDashboardWidget,
20
+ DashboardSurface,
21
+ } from '../types/plugins'
22
+ import type { FeatureDeclaration } from '../types/permissions'
23
+ import { getComponent } from '../registry'
24
+
25
+ /** The plugin contract version this runtime understands. A plugin declaring a
26
+ * higher apiVersion is rejected rather than mis-rendered. */
27
+ export const PLUGIN_API_VERSION = 1
28
+
29
+ /** Resolve a plugin route/widget/settings/onboarding target to a component,
30
+ * whether it provided the component directly or a registered component id. */
31
+ export function resolvePluginComponent(
32
+ def: { component?: React.ComponentType<unknown>; componentId?: string },
33
+ ): React.ComponentType<unknown> | undefined {
34
+ if (def.component) return def.component
35
+ if (def.componentId) return getComponent(def.componentId) as React.ComponentType<unknown> | undefined
36
+ return undefined
37
+ }
38
+
39
+ interface ResolvePluginRuntimeOptions {
40
+ plugins?: PluginManifest[]
41
+ tenantPlugins?: TenantPluginBinding[]
42
+ hasTenantBindings?: boolean
43
+ context: PluginRuntimeContext
44
+ }
45
+
46
+ const EMPTY_CONTEXT: PluginRuntimeContext = {
47
+ tenant: null,
48
+ user: null,
49
+ currentPath: '/',
50
+ matchedPath: '/',
51
+ layout: 'sidebar',
52
+ }
53
+
54
+ function slugify(input: string): string {
55
+ return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
56
+ }
57
+
58
+ function matchesRoutePattern(pattern: string, currentPath: string): boolean {
59
+ if (pattern === currentPath) return true
60
+ if (pattern.endsWith('/*')) {
61
+ const base = pattern.slice(0, -2)
62
+ return currentPath === base || currentPath.startsWith(`${base}/`)
63
+ }
64
+ if (pattern.endsWith('*')) return currentPath.startsWith(pattern.slice(0, -1))
65
+ return false
66
+ }
67
+
68
+ function isWidgetVisible(widget: ResolvedPluginWidget, context: PluginRuntimeContext): boolean {
69
+ if (widget.permission && context.hasPermission && !context.hasPermission(widget.permission)) return false
70
+ const visibility: PluginWidgetVisibility | undefined = widget.visibility
71
+ if (!visibility) return true
72
+
73
+ if (visibility.layouts && !visibility.layouts.includes(context.layout)) return false
74
+ if (visibility.roles && context.user?.role && !visibility.roles.includes(context.user.role)) return false
75
+ if (visibility.roles && !context.user?.role) return false
76
+ if (visibility.plans && context.tenant?.plan && !visibility.plans.includes(context.tenant.plan)) return false
77
+ if (visibility.plans && !context.tenant?.plan) return false
78
+ if (visibility.routes && !visibility.routes.some((p) => matchesRoutePattern(p, context.currentPath))) return false
79
+ if (visibility.excludeRoutes?.some((p) => matchesRoutePattern(p, context.currentPath))) return false
80
+ if (visibility.permissions?.length) {
81
+ if (!context.hasPermission) return false
82
+ if (!visibility.permissions.every((req) => context.hasPermission?.(req))) return false
83
+ }
84
+ if (visibility.when && !visibility.when(context)) return false
85
+ return true
86
+ }
87
+
88
+ function normalizeSettingsTabs(plugin: PluginManifest): PluginSettingsTab[] {
89
+ return (plugin.settings ?? []).map((entry, index) => ({
90
+ ...entry,
91
+ pluginId: entry.pluginId ?? plugin.id,
92
+ order: entry.order ?? index,
93
+ id: entry.id ?? `${plugin.id}-${slugify(entry.label)}`,
94
+ }))
95
+ }
96
+
97
+ function createEmptyRuntime(context: PluginRuntimeContext = EMPTY_CONTEXT): PluginRuntime {
98
+ return {
99
+ context,
100
+ plugins: [],
101
+ activePlugins: [],
102
+ routes: [],
103
+ navigation: [],
104
+ settingsTabs: [],
105
+ widgets: [],
106
+ dashboardWidgets: [],
107
+ capabilities: [],
108
+ aiTools: [],
109
+ registries: new Map(),
110
+ pluginFeatures: [],
111
+ issues: [],
112
+ }
113
+ }
114
+
115
+ export function definePlugin<T extends PluginManifest>(plugin: T): T {
116
+ return plugin
117
+ }
118
+
119
+ export function resolvePluginRuntime({
120
+ plugins = [],
121
+ tenantPlugins = [],
122
+ hasTenantBindings,
123
+ context,
124
+ }: ResolvePluginRuntimeOptions): PluginRuntime {
125
+ if (plugins.length === 0) return createEmptyRuntime(context)
126
+
127
+ const issues: PluginRuntimeIssue[] = []
128
+ const normalizedPlugins = new Map<string, PluginManifest>()
129
+
130
+ for (const plugin of plugins) {
131
+ if (normalizedPlugins.has(plugin.id)) {
132
+ issues.push({ type: 'duplicate_plugin', pluginId: plugin.id, message: `Plugin "${plugin.id}" registered more than once.` })
133
+ continue
134
+ }
135
+ if (plugin.apiVersion != null && plugin.apiVersion > PLUGIN_API_VERSION) {
136
+ issues.push({
137
+ type: 'incompatible_api_version',
138
+ pluginId: plugin.id,
139
+ message: `Plugin "${plugin.id}" requires plugin API v${plugin.apiVersion}, but this runtime supports v${PLUGIN_API_VERSION}. Upgrade @fayz-ai/app-runtime.`,
140
+ })
141
+ continue
142
+ }
143
+ normalizedPlugins.set(plugin.id, plugin)
144
+ }
145
+
146
+ const bindings = new Map<string, TenantPluginBinding>()
147
+ for (const b of tenantPlugins) bindings.set(b.pluginId, b)
148
+ const bindingsAreManaged = hasTenantBindings ?? tenantPlugins.length > 0
149
+
150
+ const requested = new Set<string>()
151
+ const activationReason = new Map<string, ResolvedPluginManifest['activationReason']>()
152
+
153
+ for (const plugin of normalizedPlugins.values()) {
154
+ const binding = bindings.get(plugin.id)
155
+ if (binding?.status === 'active') {
156
+ requested.add(plugin.id)
157
+ activationReason.set(plugin.id, 'tenant')
158
+ continue
159
+ }
160
+ if (plugin.defaultEnabled === true || (!bindingsAreManaged && plugin.defaultEnabled !== false)) {
161
+ requested.add(plugin.id)
162
+ activationReason.set(plugin.id, 'default')
163
+ }
164
+ }
165
+
166
+ const orderedIds: string[] = []
167
+ const visiting = new Set<string>()
168
+ const visited = new Set<string>()
169
+
170
+ function visit(pluginId: string, ancestry: string[] = []): void {
171
+ if (visited.has(pluginId)) return
172
+ const plugin = normalizedPlugins.get(pluginId)
173
+ if (!plugin) return
174
+ if (visiting.has(pluginId)) {
175
+ issues.push({ type: 'circular_dependency', pluginId, message: `Circular dependency: ${[...ancestry, pluginId].join(' -> ')}.` })
176
+ return
177
+ }
178
+ visiting.add(pluginId)
179
+ for (const depId of plugin.dependencies ?? []) {
180
+ if (!normalizedPlugins.has(depId)) {
181
+ issues.push({ type: 'missing_dependency', pluginId, dependencyId: depId, message: `Plugin "${plugin.id}" depends on missing plugin "${depId}".` })
182
+ continue
183
+ }
184
+ if (requested.has(pluginId) && !requested.has(depId)) {
185
+ requested.add(depId)
186
+ activationReason.set(depId, 'dependency')
187
+ }
188
+ visit(depId, [...ancestry, pluginId])
189
+ }
190
+ visiting.delete(pluginId)
191
+ visited.add(pluginId)
192
+ orderedIds.push(pluginId)
193
+ }
194
+
195
+ for (const id of normalizedPlugins.keys()) visit(id)
196
+
197
+ const resolvedById = new Map<string, ResolvedPluginManifest>()
198
+
199
+ for (const pluginId of orderedIds) {
200
+ const plugin = normalizedPlugins.get(pluginId)
201
+ if (!plugin) continue
202
+ const binding = bindings.get(plugin.id)
203
+ const missing = (plugin.dependencies ?? []).filter((id) => !normalizedPlugins.has(id))
204
+ const compatible = !plugin.verticalId || plugin.verticalId === context.tenant?.verticalId
205
+ if (!compatible) {
206
+ issues.push({ type: 'vertical_mismatch', pluginId: plugin.id, message: `Plugin "${plugin.id}" requires vertical "${plugin.verticalId}".` })
207
+ }
208
+ const reason = activationReason.get(plugin.id) ?? 'inactive'
209
+ const depsReady = (plugin.dependencies ?? []).every((id) => resolvedById.get(id)?.isActive)
210
+ const explicitlyDisabled = binding?.status === 'disabled' || binding?.status === 'removed'
211
+ const shouldActivate = reason === 'dependency' ? true : requested.has(plugin.id) && !explicitlyDisabled
212
+ const isActive = shouldActivate && compatible && missing.length === 0 && depsReady
213
+
214
+ resolvedById.set(plugin.id, {
215
+ ...plugin,
216
+ status: isActive ? 'active' : (binding?.status ?? 'disabled'),
217
+ isActive,
218
+ activationReason: isActive ? reason : 'inactive',
219
+ config: binding?.config ?? {},
220
+ missingDependencies: missing,
221
+ settingsTabs: normalizeSettingsTabs(plugin),
222
+ widgets: plugin.widgets ?? [],
223
+ resolvedRegistries: plugin.registries ?? [],
224
+ resolvedAITools: plugin.aiTools ?? [],
225
+ })
226
+ }
227
+
228
+ const resolvedPlugins = orderedIds
229
+ .map((id) => resolvedById.get(id))
230
+ .filter((p): p is ResolvedPluginManifest => Boolean(p))
231
+
232
+ const activePlugins = resolvedPlugins.filter((p) => p.isActive)
233
+ const routes: PluginRuntimeRoute[] = []
234
+ const navigation: PluginNavigationEntry[] = []
235
+ const settingsTabs: PluginSettingsTab[] = []
236
+ const widgets: ResolvedPluginWidget[] = []
237
+ const dashboardWidgets: ResolvedDashboardWidget[] = []
238
+ const capabilities: PluginCapability[] = []
239
+ const aiTools: PluginAITool[] = []
240
+ const registries = new Map<string, PluginRegistryDef[]>()
241
+ const pluginFeatures: FeatureDeclaration[] = []
242
+
243
+ for (const plugin of activePlugins) {
244
+ routes.push(...plugin.routes.map((r) => ({ ...r, plugin })))
245
+ navigation.push(...plugin.navigation.map((entry, i) => ({
246
+ ...entry,
247
+ id: entry.id ?? `${plugin.id}:${entry.route}:${i}`,
248
+ icon: entry.icon ?? plugin.icon,
249
+ })))
250
+ settingsTabs.push(...plugin.settingsTabs)
251
+ capabilities.push(...(plugin.capabilities ?? []))
252
+ aiTools.push(...plugin.resolvedAITools)
253
+ if (plugin.resolvedRegistries.length > 0) registries.set(plugin.id, plugin.resolvedRegistries)
254
+ if (plugin.declaredFeatures) pluginFeatures.push(...plugin.declaredFeatures)
255
+ widgets.push(...plugin.widgets.map((w, i) => ({
256
+ ...w,
257
+ order: w.order ?? i,
258
+ config: { ...plugin.config, ...(w.props ?? {}) },
259
+ plugin,
260
+ })))
261
+ dashboardWidgets.push(...(plugin.dashboardWidgets ?? []).map((w, i) => ({
262
+ ...w,
263
+ domain: w.domain ?? plugin.id,
264
+ order: w.defaultOrder ?? i,
265
+ plugin,
266
+ })))
267
+ }
268
+
269
+ navigation.sort((a, b) => {
270
+ const order = { main: 0, secondary: 1, settings: 2 }
271
+ return (order[a.section] ?? 99) - (order[b.section] ?? 99) || a.position - b.position
272
+ })
273
+ settingsTabs.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
274
+ widgets.sort((a, b) => a.zone !== b.zone ? a.zone.localeCompare(b.zone) : a.order - b.order)
275
+ dashboardWidgets.sort((a, b) => a.order - b.order)
276
+
277
+ return { context, plugins: resolvedPlugins, activePlugins, routes, navigation, settingsTabs, widgets, dashboardWidgets, capabilities, aiTools, issues, registries, pluginFeatures }
278
+ }
279
+
280
+ export function getWidgetsForZone(
281
+ runtime: PluginRuntime,
282
+ zone: PluginWidgetZone,
283
+ contextOverrides?: Partial<PluginRuntimeContext>,
284
+ ): ResolvedPluginWidget[] {
285
+ const ctx = { ...runtime.context, ...contextOverrides }
286
+ return runtime.widgets.filter((w) => w.zone === zone && isWidgetVisible(w, ctx))
287
+ }
288
+
289
+ export function getDashboardWidgets(
290
+ runtime: PluginRuntime,
291
+ options: { surface: DashboardSurface; domain?: string; contextOverrides?: Partial<PluginRuntimeContext> },
292
+ ): ResolvedDashboardWidget[] {
293
+ const ctx = { ...runtime.context, ...options.contextOverrides }
294
+ return runtime.dashboardWidgets.filter((w) => {
295
+ const surfaces = w.surfaces ?? ['home', 'plugin-home']
296
+ if (!surfaces.includes(options.surface)) return false
297
+ if (options.domain && w.domain !== options.domain) return false
298
+ if (w.verticalId && ctx.tenant?.verticalId && w.verticalId !== ctx.tenant.verticalId) return false
299
+ if (w.permission && ctx.hasPermission && !ctx.hasPermission(w.permission)) return false
300
+ return true
301
+ })
302
+ }
303
+
304
+ const PluginRuntimeContext = React.createContext<PluginRuntime | null>(null)
305
+ export const PluginRuntimeProvider = PluginRuntimeContext.Provider
306
+
307
+ export function usePluginRuntime(): PluginRuntime {
308
+ return React.useContext(PluginRuntimeContext) ?? createEmptyRuntime()
309
+ }
310
+
311
+ export function usePluginRuntimeOptional(): PluginRuntime | null {
312
+ return React.useContext(PluginRuntimeContext)
313
+ }
@@ -0,0 +1,163 @@
1
+ import type { ComponentType } from 'react'
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Uniform registry — the indirection layer that lets a pure-data AppManifest
5
+ // reference code by string id. Generalizes the entity registry pattern to
6
+ // components, blocks, pages, metrics, scaffolds and plugins.
7
+ //
8
+ // Naming convention: SDK/plugin artifacts use a namespaced id
9
+ // (`agenda.week-view`, `crud.detail-header`); app-local custom code uses the
10
+ // reserved `custom:` prefix (`custom:tannat.HarvestBoard`). The platform never
11
+ // generates `custom:` ids — they only come from an app's registry.tsx.
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /** A JSON Schema object (loosely typed; validated by tooling, not at runtime). */
15
+ export type JsonSchema = Record<string, unknown>
16
+
17
+ export type RegistrySource = 'sdk' | 'plugin' | 'app'
18
+
19
+ export interface RegistryMeta {
20
+ /** Human label for the platform editor. */
21
+ label?: string
22
+ description?: string
23
+ /** Where this artifact came from (for introspection / conflict reporting). */
24
+ source?: RegistrySource
25
+ /** Owning plugin id, when source is 'plugin'. */
26
+ pluginId?: string
27
+ /** JSON Schema for this artifact's props/config — drives the visual editor
28
+ * and constrains AI generation. Optional but strongly encouraged. */
29
+ propsSchema?: JsonSchema
30
+ }
31
+
32
+ export interface RegistryEntry<T> {
33
+ id: string
34
+ value: T
35
+ meta: RegistryMeta
36
+ }
37
+
38
+ /** A typed, introspectable id→value map. Last registration wins (so an app's
39
+ * registry.tsx can override an SDK artifact by re-registering its id). */
40
+ export class Registry<T> {
41
+ private readonly entries = new Map<string, RegistryEntry<T>>()
42
+
43
+ constructor(readonly kind: string) {}
44
+
45
+ register(id: string, value: T, meta: RegistryMeta = {}): void {
46
+ this.entries.set(id, { id, value, meta })
47
+ }
48
+
49
+ get(id: string): T | undefined {
50
+ return this.entries.get(id)?.value
51
+ }
52
+
53
+ getEntry(id: string): RegistryEntry<T> | undefined {
54
+ return this.entries.get(id)
55
+ }
56
+
57
+ has(id: string): boolean {
58
+ return this.entries.has(id)
59
+ }
60
+
61
+ list(): RegistryEntry<T>[] {
62
+ return Array.from(this.entries.values())
63
+ }
64
+
65
+ ids(): string[] {
66
+ return Array.from(this.entries.keys())
67
+ }
68
+
69
+ clear(): void {
70
+ this.entries.clear()
71
+ }
72
+ }
73
+
74
+ /** True for app-local custom artifacts (`custom:*`). */
75
+ export function isCustomId(id: string): boolean {
76
+ return id.startsWith('custom:')
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Metric definitions (consumed by dashboard / reports / KPI panels).
81
+ // ---------------------------------------------------------------------------
82
+
83
+ export type MetricFormat = 'number' | 'currency' | 'percent' | 'duration' | 'rating'
84
+
85
+ export interface MetricContext {
86
+ tenantId?: string
87
+ /** ISO date range the metric should compute over, when applicable. */
88
+ range?: { from: string; to: string }
89
+ [key: string]: unknown
90
+ }
91
+
92
+ export interface MetricValue {
93
+ value: number
94
+ previousValue?: number
95
+ trend?: 'up' | 'down' | 'flat'
96
+ }
97
+
98
+ export interface MetricDefinition {
99
+ id: string
100
+ label?: string
101
+ category?: string
102
+ format?: MetricFormat
103
+ compute: (ctx: MetricContext) => Promise<MetricValue> | MetricValue
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Concrete registries. Components/blocks/pages/scaffolds hold React component
108
+ // types; metrics hold definitions; plugins hold opaque manifests (typed at the
109
+ // plugin layer to avoid a circular import).
110
+ // ---------------------------------------------------------------------------
111
+
112
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
113
+ type AnyComponent = ComponentType<any>
114
+
115
+ export const componentRegistry = new Registry<AnyComponent>('component')
116
+ export const blockRegistry = new Registry<AnyComponent>('block')
117
+ export const pageRegistry = new Registry<AnyComponent>('page')
118
+ export const scaffoldRegistry = new Registry<AnyComponent>('scaffold')
119
+ export const metricRegistry = new Registry<MetricDefinition>('metric')
120
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
121
+ export const pluginRegistry = new Registry<any>('plugin')
122
+
123
+ // Thin functional facade (the public API surface) -----------------------------
124
+
125
+ export const registerComponent = (id: string, c: AnyComponent, meta?: RegistryMeta) =>
126
+ componentRegistry.register(id, c, meta)
127
+ export const getComponent = (id: string) => componentRegistry.get(id)
128
+
129
+ export const registerBlock = (id: string, c: AnyComponent, meta?: RegistryMeta) =>
130
+ blockRegistry.register(id, c, meta)
131
+ export const getBlock = (id: string) => blockRegistry.get(id)
132
+ export const listBlocks = () => blockRegistry.list()
133
+
134
+ export const registerPage = (id: string, c: AnyComponent, meta?: RegistryMeta) =>
135
+ pageRegistry.register(id, c, meta)
136
+ export const getPage = (id: string) => pageRegistry.get(id)
137
+
138
+ export const registerScaffold = (id: string, c: AnyComponent, meta?: RegistryMeta) =>
139
+ scaffoldRegistry.register(id, c, meta)
140
+ export const getScaffold = (id: string) => scaffoldRegistry.get(id)
141
+
142
+ export const registerMetric = (def: MetricDefinition, meta?: RegistryMeta) =>
143
+ metricRegistry.register(def.id, def, meta)
144
+ export const getMetric = (id: string) => metricRegistry.get(id)
145
+ export const listMetrics = () => metricRegistry.list()
146
+
147
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
148
+ export const registerPlugin = (id: string, manifest: any, meta?: RegistryMeta) =>
149
+ pluginRegistry.register(id, manifest, meta)
150
+ export const getPlugin = (id: string) => pluginRegistry.get(id)
151
+ export const listPlugins = () => pluginRegistry.list()
152
+
153
+ // Plugin factories — a generated app registers each installed @fayz-ai/plugin-*
154
+ // factory by id (src/plugins.generated.ts), so a scaffold can resolve a
155
+ // manifest's PluginRef (id + JSON config) to a live PluginManifest:
156
+ // getPluginFactory(ref.id)?.(ref.config)
157
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
158
+ export type PluginFactory = (config?: Record<string, unknown>) => any
159
+ export const pluginFactoryRegistry = new Registry<PluginFactory>('plugin-factory')
160
+ export const registerPluginFactory = (id: string, factory: PluginFactory, meta?: RegistryMeta) =>
161
+ pluginFactoryRegistry.register(id, factory, meta)
162
+ export const getPluginFactory = (id: string) => pluginFactoryRegistry.get(id)
163
+ export const listPluginFactories = () => pluginFactoryRegistry.list()
@@ -0,0 +1,29 @@
1
+ export interface RouterAdapter {
2
+ navigate(to: string): void
3
+ getCurrentPath(): string
4
+ onPathChange(cb: (path: string) => void): () => void
5
+ }
6
+
7
+ export function hashRouterAdapter(): RouterAdapter {
8
+ return {
9
+ navigate(to) { window.location.hash = to },
10
+ getCurrentPath() { return window.location.hash.slice(1) || '/' },
11
+ onPathChange(cb) {
12
+ const handler = () => cb(window.location.hash.slice(1) || '/')
13
+ window.addEventListener('hashchange', handler)
14
+ return () => window.removeEventListener('hashchange', handler)
15
+ },
16
+ }
17
+ }
18
+
19
+ export function windowRouterAdapter(): RouterAdapter {
20
+ return {
21
+ navigate(to) { window.history.pushState(null, '', to) },
22
+ getCurrentPath() { return window.location.pathname },
23
+ onPathChange(cb) {
24
+ const handler = () => cb(window.location.pathname)
25
+ window.addEventListener('popstate', handler)
26
+ return () => window.removeEventListener('popstate', handler)
27
+ },
28
+ }
29
+ }
@@ -0,0 +1,15 @@
1
+ export {
2
+ createFayzRuntimeClient,
3
+ FayzRuntimeError,
4
+ } from '@fayz-ai/sdk/runtime'
5
+ export type {
6
+ FayzRuntimeClientOptions,
7
+ FayzRuntimeEnvironment,
8
+ GoogleCalendarEvent,
9
+ GoogleCalendarEventInput,
10
+ GoogleCalendarEventTime,
11
+ ListGoogleCalendarEventsInput,
12
+ PluginOAuthExchangeInput,
13
+ PluginOAuthExchangeResponse,
14
+ RuntimePluginOAuthGrant,
15
+ } from '@fayz-ai/sdk/runtime'