@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,394 @@
1
+ import type { EntityDef } from '../types/crud'
2
+ import type { LocaleConfig } from '../types/i18n'
3
+ import type { PermissionsConfig } from '../types/permissions'
4
+ import type { BillingConfig } from '../types/billing'
5
+ import type { BlockNode } from '../blocks'
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // AppManifest — THE app, as data.
9
+ //
10
+ // This is the canonical artifact every generated repo commits as
11
+ // `app.manifest.json` and that `fayz upgrade` migrates across the fleet. It is
12
+ // strictly JSON-serializable: no functions, no components — only ids resolved
13
+ // against the registries (see ../registry). `createSaasApp`/`createStorefrontApp`
14
+ // become thin sugar that builds a manifest of this shape.
15
+ //
16
+ // Versioning: bump CURRENT_MANIFEST_VERSION and register a migration whenever
17
+ // the shape changes. The single biggest fleet-maintenance lever — getting it
18
+ // right is why W1 is the "point of no return".
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export const CURRENT_MANIFEST_VERSION = 2
22
+
23
+ export type BackendProvider = 'supabase' | 'fayz-api' | 'fayz-shop' | 'mock' | 'custom'
24
+
25
+ export interface BackendRef {
26
+ provider: BackendProvider
27
+ /** Provider project ref / URL. Secrets stay in env/platform storage. */
28
+ projectRef?: string
29
+ url?: string
30
+ /** Registered adapter id when provider is 'custom'. */
31
+ adapterId?: string
32
+ /** Public, JSON-safe provider options. Never store secrets here. */
33
+ options?: Record<string, unknown>
34
+ }
35
+
36
+ /** How a manifest page produces its body — exactly one of blocks/entity/component. */
37
+ export interface PageManifest {
38
+ path: string
39
+ label?: string
40
+ icon?: string
41
+ section?: 'main' | 'secondary' | 'settings'
42
+ /** A block tree (pure data). */
43
+ blocks?: BlockNode[]
44
+ /** An entity key → rendered as a CRUD page by the scaffold. */
45
+ entity?: string
46
+ /** A registered page-component id, e.g. 'custom:HarvestBoard'. */
47
+ component?: string
48
+ /** Optional permission gate: feature + action. */
49
+ permission?: { feature: string; action: string }
50
+ }
51
+
52
+ export interface PluginRef {
53
+ /** Plugin id, resolved to an installed @fayz-ai/plugin-* at build time. */
54
+ id: string
55
+ /** Pure-data config passed to the plugin (labels, kinds, module flags…). */
56
+ config?: Record<string, unknown>
57
+ enabled?: boolean
58
+ }
59
+
60
+ export interface SurfaceManifest {
61
+ /** Registered scaffold id: 'admin' | 'storefront' | 'portal' | custom. */
62
+ scaffold: string
63
+ plugins?: PluginRef[]
64
+ pages?: PageManifest[]
65
+ /** Surface-specific declarative options (announcement, footer, nav, shipping,
66
+ * catalog seed, onboarding, …) — kept open so scaffolds evolve without a
67
+ * manifest-version bump for additive fields. */
68
+ options?: Record<string, unknown>
69
+ }
70
+
71
+ export interface AppManifest {
72
+ manifestVersion: number
73
+ id: string
74
+ name: string
75
+ backend?: BackendRef
76
+ locale?: LocaleConfig
77
+ /** Theme tokens — SaasTheme or StorefrontTheme depending on surface; kept
78
+ * loose at the core layer to avoid coupling core to surface packages. */
79
+ theme?: Record<string, unknown>
80
+ /** Keyed by surface name ('admin', 'storefront', …). At least one required. */
81
+ surfaces: Record<string, SurfaceManifest>
82
+ /** App-defined entity extensions (vertical-specific tables/fields). */
83
+ entities?: EntityDef[]
84
+ permissions?: PermissionsConfig
85
+ billing?: BillingConfig
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Migration runner — applies sequential migrations from a manifest's version
90
+ // up to CURRENT. Each migration is keyed by the version it upgrades FROM.
91
+ // ---------------------------------------------------------------------------
92
+
93
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
+ type AnyManifest = Record<string, any>
95
+ export type ManifestMigration = (m: AnyManifest) => AnyManifest
96
+
97
+ const migrations = new Map<number, ManifestMigration>()
98
+ const supportedBackendProviders: BackendProvider[] = ['supabase', 'fayz-api', 'fayz-shop', 'mock', 'custom']
99
+ const supportedPageSections = new Set(['main', 'secondary', 'settings'])
100
+ const allowedManifestKeys = new Set([
101
+ 'manifestVersion',
102
+ 'id',
103
+ 'name',
104
+ 'backend',
105
+ 'locale',
106
+ 'theme',
107
+ 'permissions',
108
+ 'billing',
109
+ 'entities',
110
+ 'surfaces',
111
+ ])
112
+ const allowedBackendKeys = new Set(['provider', 'projectRef', 'url', 'adapterId', 'options'])
113
+ const allowedSurfaceKeys = new Set(['scaffold', 'options', 'plugins', 'pages'])
114
+ const allowedPluginRefKeys = new Set(['id', 'config', 'enabled'])
115
+ const allowedPageKeys = new Set(['path', 'label', 'icon', 'section', 'blocks', 'entity', 'component', 'permission'])
116
+ const allowedPermissionKeys = new Set(['feature', 'action'])
117
+ const allowedBlockKeys = new Set(['type', 'id', 'props', 'children'])
118
+ const manifestIdPattern = /^[a-z0-9][a-z0-9-]*$/
119
+
120
+ /** Register a migration that upgrades a manifest FROM `fromVersion` to fromVersion+1. */
121
+ export function registerManifestMigration(fromVersion: number, fn: ManifestMigration): void {
122
+ migrations.set(fromVersion, fn)
123
+ }
124
+
125
+ /** Bring any manifest up to CURRENT_MANIFEST_VERSION, or throw if it was built
126
+ * for a newer SDK than this runtime understands. */
127
+ export function migrateManifest(input: AnyManifest): AppManifest {
128
+ let m = { ...input }
129
+ let version = typeof m.manifestVersion === 'number' ? m.manifestVersion : 1
130
+ if (version > CURRENT_MANIFEST_VERSION) {
131
+ throw new Error(
132
+ `Manifest version ${version} is newer than this SDK supports (${CURRENT_MANIFEST_VERSION}). Upgrade @fayz-ai/app-runtime.`,
133
+ )
134
+ }
135
+ while (version < CURRENT_MANIFEST_VERSION) {
136
+ const migrate = migrations.get(version)
137
+ if (!migrate) {
138
+ throw new Error(`No manifest migration registered from version ${version}.`)
139
+ }
140
+ m = migrate(m)
141
+ version += 1
142
+ m.manifestVersion = version
143
+ }
144
+ return m as AppManifest
145
+ }
146
+
147
+ /** Validate the structural invariants the JSON Schema can't fully express.
148
+ * Returns a list of human-readable problems (empty = valid). */
149
+ export function validateManifest(m: AppManifest): string[] {
150
+ const problems: string[] = []
151
+ const manifest = m as unknown as Record<string, unknown>
152
+
153
+ addUnsupportedKeys(manifest, allowedManifestKeys, 'manifest', problems)
154
+
155
+ if (!Number.isInteger(manifest.manifestVersion)) {
156
+ problems.push('manifest.manifestVersion must be an integer')
157
+ } else if (manifest.manifestVersion !== CURRENT_MANIFEST_VERSION) {
158
+ problems.push(`manifest.manifestVersion must be ${CURRENT_MANIFEST_VERSION}`)
159
+ }
160
+ if (typeof manifest.id !== 'string' || !manifest.id.trim()) problems.push('manifest.id is required')
161
+ else if (!manifestIdPattern.test(manifest.id.trim())) {
162
+ problems.push('manifest.id must match /^[a-z0-9][a-z0-9-]*$/')
163
+ }
164
+ if (typeof manifest.name !== 'string' || !manifest.name.trim()) problems.push('manifest.name is required')
165
+
166
+ validateLooseObject(manifest.locale, 'manifest.locale', problems)
167
+ validateLooseObject(manifest.theme, 'manifest.theme', problems)
168
+ validateLooseObject(manifest.permissions, 'manifest.permissions', problems)
169
+ validateLooseObject(manifest.billing, 'manifest.billing', problems)
170
+ if (manifest.entities !== undefined) {
171
+ if (!Array.isArray(manifest.entities)) {
172
+ problems.push('manifest.entities must be an array')
173
+ } else {
174
+ manifest.entities.forEach((entity, index) => {
175
+ if (!isRecord(entity)) problems.push(`manifest.entities[${index}] must be an object`)
176
+ })
177
+ }
178
+ }
179
+
180
+ if (manifest.backend !== undefined) {
181
+ if (!isRecord(manifest.backend)) {
182
+ problems.push('manifest.backend must be an object')
183
+ } else {
184
+ addUnsupportedKeys(manifest.backend, allowedBackendKeys, 'manifest.backend', problems)
185
+ const provider = manifest.backend.provider
186
+ if (typeof provider !== 'string' || !supportedBackendProviders.includes(provider as BackendProvider)) {
187
+ problems.push(`manifest.backend.provider "${String(provider)}" is not supported`)
188
+ }
189
+ validateOptionalNonEmptyString(manifest.backend.projectRef, 'manifest.backend.projectRef', problems)
190
+ validateOptionalNonEmptyString(manifest.backend.url, 'manifest.backend.url', problems)
191
+ if (provider === 'custom' && !isNonEmptyString(manifest.backend.adapterId)) {
192
+ problems.push('manifest.backend.adapterId is required when provider is "custom"')
193
+ } else if (provider !== 'custom') {
194
+ validateOptionalNonEmptyString(manifest.backend.adapterId, 'manifest.backend.adapterId', problems)
195
+ }
196
+ validateLooseObject(manifest.backend.options, 'manifest.backend.options', problems)
197
+ }
198
+ }
199
+
200
+ if (!isRecord(manifest.surfaces) || Object.keys(manifest.surfaces).length === 0) {
201
+ problems.push('manifest.surfaces must declare at least one surface')
202
+ } else {
203
+ for (const [name, surface] of Object.entries(manifest.surfaces)) {
204
+ validateSurfaceManifest(name, surface, problems)
205
+ }
206
+ }
207
+
208
+ return problems
209
+ }
210
+
211
+ function validateSurfaceManifest(name: string, surface: unknown, problems: string[]): void {
212
+ if (!isRecord(surface)) {
213
+ problems.push(`surface "${name}" must be an object`)
214
+ return
215
+ }
216
+
217
+ addUnsupportedKeys(surface, allowedSurfaceKeys, `surface "${name}"`, problems)
218
+ if (!isNonEmptyString(surface.scaffold)) problems.push(`surface "${name}" is missing a scaffold id`)
219
+ validateLooseObject(surface.options, `surface "${name}".options`, problems)
220
+
221
+ if (surface.pages !== undefined) {
222
+ if (!Array.isArray(surface.pages)) {
223
+ problems.push(`surface "${name}".pages must be an array`)
224
+ } else {
225
+ const pagePaths = new Set<string>()
226
+ for (const [index, page] of surface.pages.entries()) {
227
+ validatePageManifest(name, index, page, pagePaths, problems)
228
+ }
229
+ }
230
+ }
231
+
232
+ if (surface.plugins !== undefined) {
233
+ if (!Array.isArray(surface.plugins)) {
234
+ problems.push(`surface "${name}".plugins must be an array`)
235
+ } else {
236
+ const pluginIds = new Set<string>()
237
+ for (const [index, plugin] of surface.plugins.entries()) {
238
+ validatePluginRef(name, index, plugin, pluginIds, problems)
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ function validatePageManifest(
245
+ surfaceName: string,
246
+ index: number,
247
+ page: unknown,
248
+ pagePaths: Set<string>,
249
+ problems: string[],
250
+ ): void {
251
+ const pageLabel = `surface "${surfaceName}" page #${index + 1}`
252
+ if (!isRecord(page)) {
253
+ problems.push(`${pageLabel} must be an object`)
254
+ return
255
+ }
256
+
257
+ addUnsupportedKeys(page, allowedPageKeys, pageLabel, problems)
258
+
259
+ if (!isNonEmptyString(page.path)) {
260
+ problems.push(`${pageLabel} has an empty path`)
261
+ } else {
262
+ const normalizedPath = page.path.trim()
263
+ if (pagePaths.has(normalizedPath)) {
264
+ problems.push(`surface "${surfaceName}" declares duplicate page path "${normalizedPath}"`)
265
+ } else {
266
+ pagePaths.add(normalizedPath)
267
+ }
268
+ }
269
+
270
+ if (page.label !== undefined && typeof page.label !== 'string') problems.push(`${pageLabel}.label must be a string`)
271
+ if (page.icon !== undefined && typeof page.icon !== 'string') problems.push(`${pageLabel}.icon must be a string`)
272
+ if (page.section !== undefined && (typeof page.section !== 'string' || !supportedPageSections.has(page.section))) {
273
+ problems.push(`${pageLabel}.section must be one of main, secondary, settings`)
274
+ }
275
+ if (page.entity !== undefined && !isNonEmptyString(page.entity)) problems.push(`${pageLabel}.entity must be a non-empty string`)
276
+ if (page.component !== undefined && !isNonEmptyString(page.component)) {
277
+ problems.push(`${pageLabel}.component must be a non-empty string`)
278
+ }
279
+
280
+ const kinds = [page.blocks, page.entity, page.component].filter((x) => x != null)
281
+ if (kinds.length !== 1) {
282
+ problems.push(`${pageLabel} must set exactly one of blocks/entity/component`)
283
+ }
284
+
285
+ if (page.permission !== undefined) {
286
+ validatePermission(page.permission, `${pageLabel}.permission`, problems)
287
+ }
288
+
289
+ if (page.blocks !== undefined) {
290
+ if (!Array.isArray(page.blocks)) {
291
+ problems.push(`${pageLabel}.blocks must be an array`)
292
+ } else {
293
+ page.blocks.forEach((block, blockIndex) => {
294
+ validateBlockManifest(block, `${pageLabel}.blocks[${blockIndex}]`, problems)
295
+ })
296
+ }
297
+ }
298
+ }
299
+
300
+ function validatePluginRef(
301
+ surfaceName: string,
302
+ index: number,
303
+ plugin: unknown,
304
+ pluginIds: Set<string>,
305
+ problems: string[],
306
+ ): void {
307
+ const pluginLabel = `surface "${surfaceName}" plugin #${index + 1}`
308
+ if (!isRecord(plugin)) {
309
+ problems.push(`${pluginLabel} must be an object`)
310
+ return
311
+ }
312
+
313
+ addUnsupportedKeys(plugin, allowedPluginRefKeys, pluginLabel, problems)
314
+
315
+ if (!isNonEmptyString(plugin.id)) {
316
+ problems.push(`${pluginLabel} has an empty id`)
317
+ } else {
318
+ const normalizedId = plugin.id.trim()
319
+ if (pluginIds.has(normalizedId)) {
320
+ problems.push(`surface "${surfaceName}" declares duplicate plugin id "${normalizedId}"`)
321
+ } else {
322
+ pluginIds.add(normalizedId)
323
+ }
324
+ }
325
+
326
+ validateLooseObject(plugin.config, `${pluginLabel}.config`, problems)
327
+ if (plugin.enabled !== undefined && typeof plugin.enabled !== 'boolean') {
328
+ problems.push(`${pluginLabel}.enabled must be a boolean`)
329
+ }
330
+ }
331
+
332
+ function validatePermission(permission: unknown, path: string, problems: string[]): void {
333
+ if (!isRecord(permission)) {
334
+ problems.push(`${path} must be an object`)
335
+ return
336
+ }
337
+
338
+ addUnsupportedKeys(permission, allowedPermissionKeys, path, problems)
339
+ if (!isNonEmptyString(permission.feature)) problems.push(`${path}.feature is required`)
340
+ if (!isNonEmptyString(permission.action)) problems.push(`${path}.action is required`)
341
+ }
342
+
343
+ function validateBlockManifest(block: unknown, path: string, problems: string[]): void {
344
+ if (!isRecord(block)) {
345
+ problems.push(`${path} must be an object`)
346
+ return
347
+ }
348
+
349
+ addUnsupportedKeys(block, allowedBlockKeys, path, problems)
350
+ if (!isNonEmptyString(block.type)) problems.push(`${path}.type is required`)
351
+ validateLooseObject(block.props, `${path}.props`, problems)
352
+ if (block.id !== undefined && typeof block.id !== 'string') problems.push(`${path}.id must be a string`)
353
+
354
+ if (block.children !== undefined) {
355
+ if (!Array.isArray(block.children)) {
356
+ problems.push(`${path}.children must be an array`)
357
+ } else {
358
+ block.children.forEach((child, index) => validateBlockManifest(child, `${path}.children[${index}]`, problems))
359
+ }
360
+ }
361
+ }
362
+
363
+ function addUnsupportedKeys(
364
+ value: Record<string, unknown>,
365
+ allowedKeys: Set<string>,
366
+ path: string,
367
+ problems: string[],
368
+ ): void {
369
+ for (const key of Object.keys(value)) {
370
+ if (!allowedKeys.has(key)) {
371
+ problems.push(`${path}.${key} is not part of AppManifest v2`)
372
+ }
373
+ }
374
+ }
375
+
376
+ function validateLooseObject(value: unknown, path: string, problems: string[]): void {
377
+ if (value !== undefined && !isRecord(value)) {
378
+ problems.push(`${path} must be an object`)
379
+ }
380
+ }
381
+
382
+ function validateOptionalNonEmptyString(value: unknown, path: string, problems: string[]): void {
383
+ if (value !== undefined && !isNonEmptyString(value)) {
384
+ problems.push(`${path} must be a non-empty string`)
385
+ }
386
+ }
387
+
388
+ function isRecord(value: unknown): value is Record<string, unknown> {
389
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
390
+ }
391
+
392
+ function isNonEmptyString(value: unknown): value is string {
393
+ return typeof value === 'string' && value.trim().length > 0
394
+ }
@@ -0,0 +1,30 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { getSupabaseClientOptional } from '../data/supabase'
3
+
4
+ /**
5
+ * createSafeDataProvider — the standard lazy provider resolver every plugin
6
+ * repeated by hand. Returns a Proxy that, on first method call, resolves to the
7
+ * Supabase-backed provider when a Supabase client is configured, otherwise the
8
+ * mock provider. Resolution is deferred so importing a plugin never forces a
9
+ * Supabase connection, and memoized so it only happens once.
10
+ *
11
+ * const provider = options?.dataProvider ?? createSafeDataProvider(
12
+ * () => createSupabaseCrmProvider(providerOptions),
13
+ * () => createMockCrmProvider(),
14
+ * )
15
+ */
16
+ export function createSafeDataProvider<T extends object>(
17
+ makeSupabase: () => T,
18
+ makeMock: () => T,
19
+ ): T {
20
+ let resolved: T | null = null
21
+ const get = (): T => {
22
+ if (!resolved) {
23
+ resolved = getSupabaseClientOptional() ? makeSupabase() : makeMock()
24
+ }
25
+ return resolved
26
+ }
27
+ return new Proxy({} as T, {
28
+ get: (_target, prop) => (...args: any[]) => (get() as any)[prop](...args),
29
+ })
30
+ }
@@ -0,0 +1 @@
1
+ export { definePlugin, resolvePluginRuntime, getWidgetsForZone, PluginRuntimeProvider, usePluginRuntime, usePluginRuntimeOptional } from './runtime'