@fayz-ai/core 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/denial.d.ts +21 -0
- package/dist/access/denial.d.ts.map +1 -0
- package/dist/access/engine.d.ts +63 -0
- package/dist/access/engine.d.ts.map +1 -0
- package/dist/access/index.cjs +60 -0
- package/dist/access/index.cjs.map +1 -0
- package/dist/access/index.d.ts +6 -0
- package/dist/access/index.d.ts.map +1 -0
- package/dist/access/index.js +51 -0
- package/dist/access/index.js.map +1 -0
- package/dist/access/limits.d.ts +10 -0
- package/dist/access/limits.d.ts.map +1 -0
- package/dist/address/index.d.ts +52 -0
- package/dist/address/index.d.ts.map +1 -0
- package/dist/{chunk-WKZVSKEU.js → chunk-CU64QI4A.js} +2 -2
- package/dist/chunk-CU64QI4A.js.map +1 -0
- package/dist/{chunk-54SRL7AJ.cjs → chunk-CYAWDW44.cjs} +141 -84
- package/dist/chunk-CYAWDW44.cjs.map +1 -0
- package/dist/{chunk-KNIVT6XZ.js → chunk-JHGXROKV.js} +230 -2
- package/dist/chunk-JHGXROKV.js.map +1 -0
- package/dist/{chunk-QR7U2URY.cjs → chunk-SO3VUKGZ.cjs} +230 -2
- package/dist/chunk-SO3VUKGZ.cjs.map +1 -0
- package/dist/{chunk-2NTHNWEH.cjs → chunk-UKU4RIWM.cjs} +2 -2
- package/dist/chunk-UKU4RIWM.cjs.map +1 -0
- package/dist/{chunk-ZBIWYDAR.js → chunk-XJKW2JRW.js} +140 -85
- package/dist/chunk-XJKW2JRW.js.map +1 -0
- package/dist/{chunk-CHGX6UYR.js → chunk-YH25Y4FW.js} +6 -3
- package/dist/chunk-YH25Y4FW.js.map +1 -0
- package/dist/{chunk-5X2VX3RQ.cjs → chunk-ZOPJB5FN.cjs} +6 -3
- package/dist/chunk-ZOPJB5FN.cjs.map +1 -0
- package/dist/data/count.d.ts +34 -0
- package/dist/data/count.d.ts.map +1 -0
- package/dist/data/index.cjs +18 -10
- package/dist/data/index.d.ts +2 -0
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +1 -1
- package/dist/data/resolve.d.ts.map +1 -1
- package/dist/entity/index.cjs +6 -6
- package/dist/entity/index.js +1 -1
- package/dist/entity/registry.d.ts +9 -0
- package/dist/entity/registry.d.ts.map +1 -1
- package/dist/i18n/index.cjs +10 -10
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/shell-translations.d.ts.map +1 -1
- package/dist/index.cjs +852 -124
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +765 -56
- package/dist/index.js.map +1 -1
- package/dist/manifest/index.d.ts +109 -3
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/serialize.d.ts +14 -0
- package/dist/manifest/serialize.d.ts.map +1 -0
- package/dist/plugin/index.cjs +7 -7
- package/dist/plugin/index.js +1 -1
- package/dist/plugin/runtime.d.ts.map +1 -1
- package/dist/types/billing.d.ts +20 -0
- package/dist/types/billing.d.ts.map +1 -1
- package/dist/types/crud.d.ts +48 -2
- package/dist/types/crud.d.ts.map +1 -1
- package/dist/types/entitlements.d.ts +45 -0
- package/dist/types/entitlements.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/plugins.d.ts +84 -0
- package/dist/types/plugins.d.ts.map +1 -1
- package/package.json +14 -4
- package/src/access/denial.ts +33 -0
- package/src/access/engine.test.ts +48 -0
- package/src/access/engine.ts +121 -0
- package/src/access/index.ts +13 -0
- package/src/access/limits.ts +18 -0
- package/src/address/index.ts +188 -0
- package/src/data/count.test.ts +152 -0
- package/src/data/count.ts +131 -0
- package/src/data/index.ts +2 -0
- package/src/data/resolve.ts +7 -1
- package/src/entity/registry.ts +7 -0
- package/src/i18n/shell-translations.ts +230 -0
- package/src/index.ts +18 -3
- package/src/manifest/app-manifest.schema.json +618 -45
- package/src/manifest/index.ts +127 -3
- package/src/manifest/serialize.ts +52 -0
- package/src/plugin/runtime.ts +5 -1
- package/src/types/billing.ts +22 -0
- package/src/types/crud.ts +49 -2
- package/src/types/entitlements.ts +45 -0
- package/src/types/index.ts +4 -3
- package/src/types/plugins.ts +79 -0
- package/dist/chunk-2NTHNWEH.cjs.map +0 -1
- package/dist/chunk-54SRL7AJ.cjs.map +0 -1
- package/dist/chunk-5X2VX3RQ.cjs.map +0 -1
- package/dist/chunk-CHGX6UYR.js.map +0 -1
- package/dist/chunk-KNIVT6XZ.js.map +0 -1
- package/dist/chunk-QR7U2URY.cjs.map +0 -1
- package/dist/chunk-WKZVSKEU.js.map +0 -1
- package/dist/chunk-ZBIWYDAR.js.map +0 -1
package/src/manifest/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { EntityDef } from '../types/crud'
|
|
2
2
|
import type { LocaleConfig } from '../types/i18n'
|
|
3
|
-
import type { PermissionsConfig } from '../types/permissions'
|
|
3
|
+
import type { PermissionsConfig, FeatureDeclaration } from '../types/permissions'
|
|
4
4
|
import type { BillingConfig } from '../types/billing'
|
|
5
|
+
import type { LimitDeclaration } from '../types/entitlements'
|
|
6
|
+
import type { AIToolParameters, AIToolExecution, AgentRpcDeclaration } from '../types/plugins'
|
|
5
7
|
import type { BlockNode } from '../blocks'
|
|
6
8
|
|
|
7
9
|
// ---------------------------------------------------------------------------
|
|
@@ -18,7 +20,7 @@ import type { BlockNode } from '../blocks'
|
|
|
18
20
|
// right is why W1 is the "point of no return".
|
|
19
21
|
// ---------------------------------------------------------------------------
|
|
20
22
|
|
|
21
|
-
export const CURRENT_MANIFEST_VERSION =
|
|
23
|
+
export const CURRENT_MANIFEST_VERSION = 3
|
|
22
24
|
|
|
23
25
|
export type BackendProvider = 'supabase' | 'fayz-api' | 'fayz-shop' | 'mock' | 'custom'
|
|
24
26
|
|
|
@@ -68,6 +70,97 @@ export interface SurfaceManifest {
|
|
|
68
70
|
options?: Record<string, unknown>
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Agent Contract (manifest v3) — everything a SERVER-SIDE agent runtime needs
|
|
75
|
+
// to operate the app without a browser: the full entity surface, the tool
|
|
76
|
+
// catalog with its authorization requirements, the pool RPCs writes go
|
|
77
|
+
// through, and the structured domain knowledge the app wants in the prompt.
|
|
78
|
+
// Derived from app code at publish (never hand-written) and synced to the
|
|
79
|
+
// Fayz platform, where it becomes the versioned AgentContract the broker
|
|
80
|
+
// validates every turn against.
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/** JSON-safe projection of an EntityDef — schema only, no components/closures.
|
|
84
|
+
* `data` mirrors EntityDef.data: everything the server-plane read executor
|
|
85
|
+
* needs (tenant scoping, search/select column allowlists, fixed filters). */
|
|
86
|
+
export interface EntityContract {
|
|
87
|
+
key: string
|
|
88
|
+
label: string
|
|
89
|
+
labelPlural?: string
|
|
90
|
+
/** Plan-limit key rows of this entity consume (EntityDef.limitKey). */
|
|
91
|
+
limitKey?: string
|
|
92
|
+
/** Permission to READ via the generic data primitives (EntityDef.permission). */
|
|
93
|
+
permission?: { feature: string; action: string }
|
|
94
|
+
data: {
|
|
95
|
+
table: string
|
|
96
|
+
schema?: string
|
|
97
|
+
tenantScoped?: boolean
|
|
98
|
+
tenantIdColumn?: string
|
|
99
|
+
searchColumns?: string[]
|
|
100
|
+
selectColumns?: string
|
|
101
|
+
columnMap?: Record<string, string>
|
|
102
|
+
archetype?: string
|
|
103
|
+
archetypeKind?: string
|
|
104
|
+
filters?: Record<string, string>
|
|
105
|
+
}
|
|
106
|
+
fields: Array<{
|
|
107
|
+
key: string
|
|
108
|
+
label: string
|
|
109
|
+
type: string
|
|
110
|
+
required?: boolean
|
|
111
|
+
searchable?: boolean
|
|
112
|
+
/** FieldDef.hint — the field's helper text. Carried into the contract
|
|
113
|
+
* because it is what tells the agent what a non-obvious column expects:
|
|
114
|
+
* an id to look up, a default it should not restate, a format. */
|
|
115
|
+
hint?: string
|
|
116
|
+
/** Allowed values for select-like fields. */
|
|
117
|
+
options?: string[]
|
|
118
|
+
/** Table the relation field points at (FieldRelation.table). */
|
|
119
|
+
relationTable?: string
|
|
120
|
+
}>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** JSON-safe projection of a PluginAITool + its owning plugin. */
|
|
124
|
+
export interface AIToolContract {
|
|
125
|
+
id: string
|
|
126
|
+
name: string
|
|
127
|
+
description: string
|
|
128
|
+
mode: 'read' | 'persist'
|
|
129
|
+
parameters?: AIToolParameters
|
|
130
|
+
permission?: { feature: string; action: string }
|
|
131
|
+
limitKey?: string
|
|
132
|
+
execution?: AIToolExecution
|
|
133
|
+
requiresConfirmation?: boolean
|
|
134
|
+
pluginId?: string
|
|
135
|
+
category?: string
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** A tenant-scoped pool RPC the server-plane executor may call. Canonical
|
|
139
|
+
* definition lives with the plugin types (plugins declare these). */
|
|
140
|
+
export type { AgentRpcDeclaration as RpcContract } from '../types/plugins'
|
|
141
|
+
|
|
142
|
+
/** Structured domain knowledge rendered into the agent's prompt as labelled
|
|
143
|
+
* sections — statuses with transitions, scheduling defaults, free-form rules. */
|
|
144
|
+
export interface AgentDomainKnowledge {
|
|
145
|
+
statuses?: Record<string, Array<{ id: string; label: string; availableWhen?: unknown }>>
|
|
146
|
+
scheduleDefaults?: Record<string, unknown>
|
|
147
|
+
businessRules?: Array<{ id: string; description: string }>
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface AgentContract {
|
|
151
|
+
persona?: { name?: string; systemPrompt?: string; icon?: string }
|
|
152
|
+
/** When 'server', surfaces stop shipping data-tool executors: the broker
|
|
153
|
+
* executes reads/writes server-plane and the surface only handles
|
|
154
|
+
* client-plane tools. Flipped per app — no lockstep release. */
|
|
155
|
+
executionPlane?: 'client' | 'server'
|
|
156
|
+
entities?: EntityContract[]
|
|
157
|
+
tools?: AIToolContract[]
|
|
158
|
+
registries?: Array<{ pluginId: string; id: string; entityKey?: string; readOnly?: boolean }>
|
|
159
|
+
declaredFeatures?: FeatureDeclaration[]
|
|
160
|
+
rpcs?: AgentRpcDeclaration[]
|
|
161
|
+
domainKnowledge?: AgentDomainKnowledge
|
|
162
|
+
}
|
|
163
|
+
|
|
71
164
|
export interface AppManifest {
|
|
72
165
|
manifestVersion: number
|
|
73
166
|
id: string
|
|
@@ -83,6 +176,13 @@ export interface AppManifest {
|
|
|
83
176
|
entities?: EntityDef[]
|
|
84
177
|
permissions?: PermissionsConfig
|
|
85
178
|
billing?: BillingConfig
|
|
179
|
+
/** The RESOLVED limit declarations (4-layer merge: core < entity-derived <
|
|
180
|
+
* plugin < app), so a server runtime counts exactly what the client counts. */
|
|
181
|
+
limitDeclarations?: LimitDeclaration[]
|
|
182
|
+
/** v3: the agent contract — see the section comment above. */
|
|
183
|
+
agent?: AgentContract
|
|
184
|
+
/** sha256 of the canonical serialization (excluding this field). */
|
|
185
|
+
contractHash?: string
|
|
86
186
|
}
|
|
87
187
|
|
|
88
188
|
// ---------------------------------------------------------------------------
|
|
@@ -108,6 +208,9 @@ const allowedManifestKeys = new Set([
|
|
|
108
208
|
'billing',
|
|
109
209
|
'entities',
|
|
110
210
|
'surfaces',
|
|
211
|
+
'limitDeclarations',
|
|
212
|
+
'agent',
|
|
213
|
+
'contractHash',
|
|
111
214
|
])
|
|
112
215
|
const allowedBackendKeys = new Set(['provider', 'projectRef', 'url', 'adapterId', 'options'])
|
|
113
216
|
const allowedSurfaceKeys = new Set(['scaffold', 'options', 'plugins', 'pages'])
|
|
@@ -122,6 +225,12 @@ export function registerManifestMigration(fromVersion: number, fn: ManifestMigra
|
|
|
122
225
|
migrations.set(fromVersion, fn)
|
|
123
226
|
}
|
|
124
227
|
|
|
228
|
+
// v2 → v3 is purely additive (limitDeclarations/agent/contractHash are optional):
|
|
229
|
+
// a v2 manifest is a valid v3 manifest with those sections absent. Registered
|
|
230
|
+
// here so every consumer migrates for free; `fayz manifest emit` fills the new
|
|
231
|
+
// sections on the next regeneration.
|
|
232
|
+
registerManifestMigration(2, (m) => m)
|
|
233
|
+
|
|
125
234
|
/** Bring any manifest up to CURRENT_MANIFEST_VERSION, or throw if it was built
|
|
126
235
|
* for a newer SDK than this runtime understands. */
|
|
127
236
|
export function migrateManifest(input: AnyManifest): AppManifest {
|
|
@@ -167,6 +276,21 @@ export function validateManifest(m: AppManifest): string[] {
|
|
|
167
276
|
validateLooseObject(manifest.theme, 'manifest.theme', problems)
|
|
168
277
|
validateLooseObject(manifest.permissions, 'manifest.permissions', problems)
|
|
169
278
|
validateLooseObject(manifest.billing, 'manifest.billing', problems)
|
|
279
|
+
validateLooseObject(manifest.agent, 'manifest.agent', problems)
|
|
280
|
+
if (manifest.contractHash !== undefined && !isNonEmptyString(manifest.contractHash)) {
|
|
281
|
+
problems.push('manifest.contractHash must be a non-empty string')
|
|
282
|
+
}
|
|
283
|
+
if (manifest.limitDeclarations !== undefined) {
|
|
284
|
+
if (!Array.isArray(manifest.limitDeclarations)) {
|
|
285
|
+
problems.push('manifest.limitDeclarations must be an array')
|
|
286
|
+
} else {
|
|
287
|
+
manifest.limitDeclarations.forEach((decl, index) => {
|
|
288
|
+
if (!isRecord(decl) || !isNonEmptyString(decl.key) || !isNonEmptyString(decl.table)) {
|
|
289
|
+
problems.push(`manifest.limitDeclarations[${index}] must declare key and table`)
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
}
|
|
170
294
|
if (manifest.entities !== undefined) {
|
|
171
295
|
if (!Array.isArray(manifest.entities)) {
|
|
172
296
|
problems.push('manifest.entities must be an array')
|
|
@@ -368,7 +492,7 @@ function addUnsupportedKeys(
|
|
|
368
492
|
): void {
|
|
369
493
|
for (const key of Object.keys(value)) {
|
|
370
494
|
if (!allowedKeys.has(key)) {
|
|
371
|
-
problems.push(`${path}.${key} is not part of AppManifest
|
|
495
|
+
problems.push(`${path}.${key} is not part of AppManifest v${CURRENT_MANIFEST_VERSION}`)
|
|
372
496
|
}
|
|
373
497
|
}
|
|
374
498
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { AppManifest } from './index'
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Deterministic serialization — the SAME manifest object must always produce
|
|
5
|
+
// the SAME bytes, because the sha256 of those bytes (`contractHash`) is what
|
|
6
|
+
// makes `fayz manifest sync` idempotent and lets `fayz doctor` prove the
|
|
7
|
+
// committed app.manifest.json is not stale. Rules: object keys sorted, arrays
|
|
8
|
+
// kept in declared order (order is meaning: nav position, migration order),
|
|
9
|
+
// 2-space indent, trailing newline, `contractHash` itself excluded.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
function sortValue(value: unknown): unknown {
|
|
13
|
+
if (Array.isArray(value)) return value.map(sortValue)
|
|
14
|
+
if (value !== null && typeof value === 'object') {
|
|
15
|
+
const record = value as Record<string, unknown>
|
|
16
|
+
const out: Record<string, unknown> = {}
|
|
17
|
+
for (const key of Object.keys(record).sort()) {
|
|
18
|
+
const v = record[key]
|
|
19
|
+
if (v !== undefined) out[key] = sortValue(v)
|
|
20
|
+
}
|
|
21
|
+
return out
|
|
22
|
+
}
|
|
23
|
+
return value
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Canonical JSON of a manifest (contractHash excluded, keys sorted). */
|
|
27
|
+
export function serializeManifest(manifest: AppManifest): string {
|
|
28
|
+
const { contractHash: _omit, ...rest } = manifest
|
|
29
|
+
return `${JSON.stringify(sortValue(rest), null, 2)}\n`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* sha256 (hex) of the canonical serialization. Async because it runs on
|
|
34
|
+
* WebCrypto (`globalThis.crypto.subtle`), which exists in every runtime we
|
|
35
|
+
* target (browsers, Node ≥ 18, edge) — no node:crypto import, keeping this
|
|
36
|
+
* module bundleable everywhere.
|
|
37
|
+
*/
|
|
38
|
+
export async function computeContractHash(manifest: AppManifest): Promise<string> {
|
|
39
|
+
const bytes = new TextEncoder().encode(serializeManifest(manifest))
|
|
40
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes)
|
|
41
|
+
return Array.from(new Uint8Array(digest))
|
|
42
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
43
|
+
.join('')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Serialize WITH the freshly computed contractHash embedded — what
|
|
47
|
+
* `fayz manifest emit` writes to app.manifest.json. */
|
|
48
|
+
export async function serializeManifestWithHash(manifest: AppManifest): Promise<string> {
|
|
49
|
+
const contractHash = await computeContractHash(manifest)
|
|
50
|
+
const { contractHash: _omit, ...rest } = manifest
|
|
51
|
+
return `${JSON.stringify(sortValue({ ...rest, contractHash }), null, 2)}\n`
|
|
52
|
+
}
|
package/src/plugin/runtime.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
DashboardSurface,
|
|
21
21
|
} from '../types/plugins'
|
|
22
22
|
import type { FeatureDeclaration } from '../types/permissions'
|
|
23
|
+
import type { LimitDeclaration } from '../types/entitlements'
|
|
23
24
|
import type { ConnectorDefinition } from '../integrations'
|
|
24
25
|
import { getComponent } from '../registry'
|
|
25
26
|
|
|
@@ -110,6 +111,7 @@ function createEmptyRuntime(context: PluginRuntimeContext = EMPTY_CONTEXT): Plug
|
|
|
110
111
|
registries: new Map(),
|
|
111
112
|
connectorsByHost: new Map(),
|
|
112
113
|
pluginFeatures: [],
|
|
114
|
+
pluginLimits: [],
|
|
113
115
|
issues: [],
|
|
114
116
|
}
|
|
115
117
|
}
|
|
@@ -242,6 +244,7 @@ export function resolvePluginRuntime({
|
|
|
242
244
|
const registries = new Map<string, PluginRegistryDef[]>()
|
|
243
245
|
const connectorsByHost = new Map<string, ConnectorDefinition[]>()
|
|
244
246
|
const pluginFeatures: FeatureDeclaration[] = []
|
|
247
|
+
const pluginLimits: LimitDeclaration[] = []
|
|
245
248
|
|
|
246
249
|
for (const plugin of activePlugins) {
|
|
247
250
|
routes.push(...plugin.routes.map((r) => ({ ...r, plugin })))
|
|
@@ -261,6 +264,7 @@ export function resolvePluginRuntime({
|
|
|
261
264
|
connectorsByHost.set(connector.hostPluginId, list)
|
|
262
265
|
}
|
|
263
266
|
if (plugin.declaredFeatures) pluginFeatures.push(...plugin.declaredFeatures)
|
|
267
|
+
if (plugin.declaredLimits) pluginLimits.push(...plugin.declaredLimits)
|
|
264
268
|
widgets.push(...plugin.widgets.map((w, i) => ({
|
|
265
269
|
...w,
|
|
266
270
|
order: w.order ?? i,
|
|
@@ -283,7 +287,7 @@ export function resolvePluginRuntime({
|
|
|
283
287
|
widgets.sort((a, b) => a.zone !== b.zone ? a.zone.localeCompare(b.zone) : a.order - b.order)
|
|
284
288
|
dashboardWidgets.sort((a, b) => a.order - b.order)
|
|
285
289
|
|
|
286
|
-
return { context, plugins: resolvedPlugins, activePlugins, routes, navigation, settingsTabs, widgets, dashboardWidgets, capabilities, aiTools, issues, registries, connectorsByHost, pluginFeatures }
|
|
290
|
+
return { context, plugins: resolvedPlugins, activePlugins, routes, navigation, settingsTabs, widgets, dashboardWidgets, capabilities, aiTools, issues, registries, connectorsByHost, pluginFeatures, pluginLimits }
|
|
287
291
|
}
|
|
288
292
|
|
|
289
293
|
export function getWidgetsForZone(
|
package/src/types/billing.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { PlanEntitlements } from './entitlements'
|
|
2
|
+
|
|
1
3
|
export interface Plan {
|
|
2
4
|
id: string
|
|
3
5
|
name: string
|
|
@@ -5,13 +7,33 @@ export interface Plan {
|
|
|
5
7
|
price: number
|
|
6
8
|
currency: string
|
|
7
9
|
interval: 'month' | 'year'
|
|
10
|
+
/** Marketing bullets shown on the pricing page (display only). */
|
|
8
11
|
features: string[]
|
|
9
12
|
highlighted?: boolean
|
|
10
13
|
stripePriceId?: string
|
|
14
|
+
/**
|
|
15
|
+
* Structured access grants for this plan (feature gates + quantity caps). This
|
|
16
|
+
* is the enforcement source of truth — distinct from `features` (display
|
|
17
|
+
* bullets). See {@link PlanEntitlements}.
|
|
18
|
+
*/
|
|
19
|
+
entitlements?: PlanEntitlements
|
|
11
20
|
}
|
|
12
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Signature of the checkout seam. When `BillingConfig.onCheckout` is defined the
|
|
24
|
+
* shell's Subscription page calls it (passing the target plan id) INSTEAD of
|
|
25
|
+
* writing `plan` straight onto the org via `adapter.updateOrg`. This is where a
|
|
26
|
+
* real payment gateway (Stripe Checkout, Pix, Mercado Pago, …) plugs in: return
|
|
27
|
+
* once the user is redirected / the intent is created, and let the webhook be the
|
|
28
|
+
* source of truth for the persisted plan. Left undefined, the shell mutates the
|
|
29
|
+
* plan optimistically (dev / self-serve / free-tier flows).
|
|
30
|
+
*/
|
|
31
|
+
export type BillingCheckoutFn = (planId: string) => Promise<void> | void
|
|
32
|
+
|
|
13
33
|
export interface BillingConfig {
|
|
14
34
|
plans: Plan[]
|
|
15
35
|
stripePublishableKey?: string
|
|
16
36
|
portalUrl?: string
|
|
37
|
+
/** Payment-gateway seam — see {@link BillingCheckoutFn}. */
|
|
38
|
+
onCheckout?: BillingCheckoutFn
|
|
17
39
|
}
|
package/src/types/crud.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type FieldType =
|
|
|
7
7
|
| 'select' | 'multiselect' | 'segmented'
|
|
8
8
|
| 'relation'
|
|
9
9
|
| 'date' | 'datetime' | 'time'
|
|
10
|
-
| 'boolean' | 'textarea'
|
|
10
|
+
| 'boolean' | 'textarea' | 'markdown'
|
|
11
11
|
| 'color' | 'computed'
|
|
12
12
|
|
|
13
13
|
/** Foreign-key source for a `relation` field: the option list is loaded from a
|
|
@@ -79,7 +79,8 @@ export interface FieldGroup {
|
|
|
79
79
|
label: string
|
|
80
80
|
description?: string
|
|
81
81
|
columns?: 1 | 2 | 3
|
|
82
|
-
/** Renders
|
|
82
|
+
/** Renders an image slot to the left of the group's fields. Functional when
|
|
83
|
+
* the EntityDef supplies `image`; a plain placeholder otherwise. */
|
|
83
84
|
imageSlot?: boolean
|
|
84
85
|
}
|
|
85
86
|
|
|
@@ -104,6 +105,28 @@ export interface DetailTab {
|
|
|
104
105
|
|
|
105
106
|
export type FormLayout = 'person' | 'product' | 'service' | 'location' | 'order' | 'subject' | 'generic'
|
|
106
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Makes a field group's `imageSlot` functional. Without this the slot renders as
|
|
110
|
+
* a decorative placeholder — it always did, which is why product forms showed an
|
|
111
|
+
* empty thumbnail even for products that have images.
|
|
112
|
+
*
|
|
113
|
+
* Upload needs the record to exist (the file is attached to its id), so the slot
|
|
114
|
+
* stays read-only in create mode and becomes editable after the first save.
|
|
115
|
+
*/
|
|
116
|
+
export interface EntityImageConfig {
|
|
117
|
+
// Rows are loosely typed here on purpose: EntityDef<T> is consumed as
|
|
118
|
+
// EntityDef<Record<string, unknown>> by the generic CRUD machinery, and a
|
|
119
|
+
// T-parameterised callback makes the whole EntityDef invariant in T.
|
|
120
|
+
/** Current image URL for this record, if any. */
|
|
121
|
+
get: (row: Record<string, any>) => string | undefined
|
|
122
|
+
/** Persist a new image and return its URL. Omit to keep the slot read-only. */
|
|
123
|
+
upload?: (file: File, row: Record<string, any>) => Promise<string>
|
|
124
|
+
/** Remove the current image. Omit to hide the remove affordance. */
|
|
125
|
+
remove?: (row: Record<string, any>) => Promise<void>
|
|
126
|
+
/** Accepted mime types. Defaults to image/*. */
|
|
127
|
+
accept?: string
|
|
128
|
+
}
|
|
129
|
+
|
|
107
130
|
export interface EntityDef<T = Record<string, unknown>> {
|
|
108
131
|
name: string
|
|
109
132
|
namePlural?: string
|
|
@@ -111,6 +134,8 @@ export interface EntityDef<T = Record<string, unknown>> {
|
|
|
111
134
|
layout?: FormLayout
|
|
112
135
|
fields: FieldDef[]
|
|
113
136
|
fieldGroups?: FieldGroup[]
|
|
137
|
+
/** Wiring for the `imageSlot` of whichever field group declares one. */
|
|
138
|
+
image?: EntityImageConfig
|
|
114
139
|
detailTabs?: DetailTab[]
|
|
115
140
|
data?: {
|
|
116
141
|
table: string
|
|
@@ -134,4 +159,26 @@ export interface EntityDef<T = Record<string, unknown>> {
|
|
|
134
159
|
/** Faceted filters shown as pills below the list search box. Each references a
|
|
135
160
|
* field whose `options` supply the pills; an "All" pill clears the filter. */
|
|
136
161
|
facets?: { field: string; allLabel?: string }[]
|
|
162
|
+
/**
|
|
163
|
+
* Plan quantity-limit key gating creation of this entity (matches a
|
|
164
|
+
* `PlanEntitlements.limits` key + a `LimitDeclaration`). When set, the CRUD
|
|
165
|
+
* engine gates the "+ New" button (LimitGate), guards the create-form submit
|
|
166
|
+
* and the CSV import (useLimitGuard) against the plan cap, and invalidates the
|
|
167
|
+
* live count after a successful create. Absent ⇒ no plan gating (RBAC only).
|
|
168
|
+
*/
|
|
169
|
+
limitKey?: string
|
|
170
|
+
/**
|
|
171
|
+
* Permission required to READ this entity through the agent's generic data
|
|
172
|
+
* primitives (searchRecords/queryData) — checked per TARGET at execution
|
|
173
|
+
* time, so one generic tool still honors per-user access. Absent ⇒ readable
|
|
174
|
+
* by any signed-in member (same as the CRUD list today).
|
|
175
|
+
*/
|
|
176
|
+
permission?: { feature: string; action: string }
|
|
177
|
+
/**
|
|
178
|
+
* Exclude this entity from the agent's data primitives. For entities whose
|
|
179
|
+
* table is an extension/join shape the generic provider can't read usefully
|
|
180
|
+
* (e.g. staff_members without a name column) — expose a queryEntities
|
|
181
|
+
* read-model instead and hide the raw one here.
|
|
182
|
+
*/
|
|
183
|
+
agentHidden?: boolean
|
|
137
184
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entitlements — the plan side of the access decision (the role side lives in
|
|
3
|
+
* permissions.ts). A plan grants FEATURES (gate a page/module) and caps LIMITS
|
|
4
|
+
* (quantity ceilings). Feature ids are the SAME id space as RBAC/nav
|
|
5
|
+
* (`permission.feature`), so a single id threads role → nav → plan.
|
|
6
|
+
*/
|
|
7
|
+
export interface PlanEntitlements {
|
|
8
|
+
/**
|
|
9
|
+
* feature id (same id as RBAC/nav) → whether the plan grants it. Absent = the
|
|
10
|
+
* plan does not gate the feature (allowed). Only an explicit `false` denies —
|
|
11
|
+
* this keeps plans additive: features unknown to a plan stay open.
|
|
12
|
+
*/
|
|
13
|
+
features?: Record<string, boolean>
|
|
14
|
+
/**
|
|
15
|
+
* limitKey → cap. `-1` = unlimited. Keys are app/plugin-defined (e.g.
|
|
16
|
+
* `clients`, `users`, `locations`, `bookings_month`). A key absent here means
|
|
17
|
+
* "no cap on this plan" (unlimited). See {@link LimitDeclaration} for how a
|
|
18
|
+
* key is bound to a countable table.
|
|
19
|
+
*/
|
|
20
|
+
limits?: Record<string, number>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Declares HOW a limit key is counted. Contributed by plugins via
|
|
25
|
+
* `PluginManifest.declaredLimits` (aggregated by the plugin runtime) and/or
|
|
26
|
+
* overridden per-app via `billing.limitDeclarations`. The access engine reads a
|
|
27
|
+
* declaration to resolve a `limitKey` → a live `count(*)` against `table` for
|
|
28
|
+
* the active tenant, with no per-entity code.
|
|
29
|
+
*/
|
|
30
|
+
export interface LimitDeclaration {
|
|
31
|
+
/** Limit key — matches a key in {@link PlanEntitlements.limits}. */
|
|
32
|
+
key: string
|
|
33
|
+
/** Human label for paywalls / usage banners (e.g. "Patients"). */
|
|
34
|
+
label: string
|
|
35
|
+
/** Table the count runs against (tenant-scoped via `tenant_id`). */
|
|
36
|
+
table: string
|
|
37
|
+
/** Optional `kind` column filter for tables that hold several entity kinds. */
|
|
38
|
+
kindFilter?: string
|
|
39
|
+
/**
|
|
40
|
+
* Counting window. `'month'` counts rows created since the start of the
|
|
41
|
+
* current month (recurring quotas like bookings/month); `'total'` (default)
|
|
42
|
+
* counts every row (stock quantities like clients/users).
|
|
43
|
+
*/
|
|
44
|
+
period?: 'month' | 'total'
|
|
45
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Re-export all core types
|
|
2
2
|
export type { EntityArchetype, BaseEntity, PersonEntity, ProductEntity, ServiceEntity, OrderEntity, TransactionEntity, LocationEntity, ScheduleEntity, CategoryEntity } from './entities'
|
|
3
3
|
export type { EntityDef, FieldDef, FieldType, FieldRelation, FieldGroup, DetailTab, FormLayout, ComputedFieldValue } from './crud'
|
|
4
|
-
export type { PluginManifest, PluginScope, PluginStatus, ResolvedPluginManifest, PluginRuntime, PluginRuntimeContext, PluginWidgetZone, PluginWidgetDefinition, PluginNavigationEntry, PluginSettingsTab, PluginRouteDefinition, PluginAITool, PluginRegistryDef, PluginMigration, PluginDiagnostic, VerticalId, ScaffoldType } from './plugins'
|
|
4
|
+
export type { PluginManifest, PluginScope, PluginStatus, ResolvedPluginManifest, PluginRuntime, PluginRuntimeContext, PluginWidgetZone, PluginWidgetDefinition, PluginNavigationEntry, PluginSettingsTab, PluginRouteDefinition, PluginAITool, AIToolMode, AIToolExecution, AgentRpcDeclaration, AIToolParameterProperty, AIToolParameters, AIToolSuggestion, PluginRegistryDef, PluginMigration, PluginDiagnostic, VerticalId, ScaffoldType } from './plugins'
|
|
5
5
|
export type { AuthAdapter, AuthUser, AuthSession } from './auth'
|
|
6
6
|
export type { OrgAdapter, Organization, OrgMember } from './org'
|
|
7
|
-
export type { PermissionsConfig, FeatureDeclaration, PermissionAction } from './permissions'
|
|
8
|
-
export type { Plan, BillingConfig } from './billing'
|
|
7
|
+
export type { PermissionsConfig, FeatureDeclaration, PermissionAction, PermissionProfile, SystemPermission } from './permissions'
|
|
8
|
+
export type { Plan, BillingConfig, BillingCheckoutFn } from './billing'
|
|
9
|
+
export type { PlanEntitlements, LimitDeclaration } from './entitlements'
|
|
9
10
|
export type { ThemeBrand, ThemeRadius, ThemeMode } from './theme'
|
|
10
11
|
export type { LocaleConfig } from './i18n'
|
package/src/types/plugins.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type React from 'react'
|
|
2
2
|
import type { EntityDef } from './crud'
|
|
3
3
|
import type { FeatureDeclaration, PermissionAction } from './permissions'
|
|
4
|
+
import type { LimitDeclaration } from './entitlements'
|
|
4
5
|
import type { ConnectorDefinition } from '../integrations'
|
|
5
6
|
|
|
6
7
|
export type VerticalId = 'beauty' | 'food' | 'health' | 'services' | 'retail' | 'education' | (string & {})
|
|
@@ -28,6 +29,14 @@ export interface PluginNavigationEntry {
|
|
|
28
29
|
icon?: string
|
|
29
30
|
badge?: string | number
|
|
30
31
|
permission?: PluginPermissionRequirement
|
|
32
|
+
/**
|
|
33
|
+
* When this entry gains sub-path children, the shell also synthesizes a child
|
|
34
|
+
* pointing back at the parent's own route (so /courses stays reachable under
|
|
35
|
+
* the "Courses" group). Set false when the parent route is a duplicate of its
|
|
36
|
+
* first child — e.g. /shop and /shop/products render the same catalog, and
|
|
37
|
+
* listing both reads as a bug. Defaults to true.
|
|
38
|
+
*/
|
|
39
|
+
indexChild?: boolean
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
export interface PluginSettingsTab {
|
|
@@ -225,6 +234,27 @@ export interface AIToolSuggestion {
|
|
|
225
234
|
verticalId?: VerticalId
|
|
226
235
|
}
|
|
227
236
|
|
|
237
|
+
export type AIToolExecution =
|
|
238
|
+
| { plane: 'server'; kind: 'rpc'; rpc: string }
|
|
239
|
+
| { plane: 'server'; kind: 'entity_read'; entity: string }
|
|
240
|
+
| { plane: 'client' }
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* A tenant-scoped pool RPC (`agent_<domain>_<verb>`) this plugin ships for the
|
|
244
|
+
* server-plane agent executor. Surfaces in the manifest as
|
|
245
|
+
* `agent.rpcs` (RpcContract) — the broker only ever calls declared functions.
|
|
246
|
+
*/
|
|
247
|
+
export interface AgentRpcDeclaration {
|
|
248
|
+
/** Function name in the pool, e.g. 'agent_agenda_create_appointment'. */
|
|
249
|
+
name: string
|
|
250
|
+
kind: 'read' | 'write'
|
|
251
|
+
description: string
|
|
252
|
+
/** JSON Schema of the p_payload argument. */
|
|
253
|
+
payloadSchema?: AIToolParameters
|
|
254
|
+
/** Whether the function writes audit_logs itself. */
|
|
255
|
+
audits?: boolean
|
|
256
|
+
}
|
|
257
|
+
|
|
228
258
|
export interface PluginAITool {
|
|
229
259
|
id: string
|
|
230
260
|
name: string
|
|
@@ -233,6 +263,31 @@ export interface PluginAITool {
|
|
|
233
263
|
mode: AIToolMode
|
|
234
264
|
parameters?: AIToolParameters
|
|
235
265
|
permission?: PluginPermissionRequirement
|
|
266
|
+
/**
|
|
267
|
+
* Quantity-limit key this tool's writes consume (e.g. 'bookings_month' for a
|
|
268
|
+
* booking-creating tool). Persist-mode tools that create countable rows MUST
|
|
269
|
+
* declare it so the agent executor can run the SAME plan-limit guard the UI
|
|
270
|
+
* runs (guard before execute, invalidate after) — an AI agent never clicks a
|
|
271
|
+
* gated button, so the declarative contract is its only gate.
|
|
272
|
+
*/
|
|
273
|
+
limitKey?: string
|
|
274
|
+
/**
|
|
275
|
+
* Where the tool runs. Server-plane is what makes browserless channels
|
|
276
|
+
* (WhatsApp, MCP) able to run the tool at all:
|
|
277
|
+
* - `{ plane:'server', kind:'rpc', rpc }` — the Fayz broker calls the named
|
|
278
|
+
* `agent_*` RPC in the tenant pool (writes / guarded reads).
|
|
279
|
+
* - `{ plane:'server', kind:'entity_read', entity }` — the broker runs the
|
|
280
|
+
* generic tenant-scoped read against the entity's table (derived tools).
|
|
281
|
+
* - `{ plane:'client' }` (or absent) — a locally registered executor runs it
|
|
282
|
+
* in the surface (navigation, modals).
|
|
283
|
+
*/
|
|
284
|
+
execution?: AIToolExecution
|
|
285
|
+
/**
|
|
286
|
+
* Require an explicit human confirmation before executing. Defaults to true
|
|
287
|
+
* for `mode: 'persist'` tools — the channel renders it natively (FAB modal,
|
|
288
|
+
* WhatsApp yes/no reply).
|
|
289
|
+
*/
|
|
290
|
+
requiresConfirmation?: boolean
|
|
236
291
|
suggestions?: AIToolSuggestion[]
|
|
237
292
|
category?: string
|
|
238
293
|
tags?: string[]
|
|
@@ -314,6 +369,28 @@ export interface PluginManifest {
|
|
|
314
369
|
entities?: string[]
|
|
315
370
|
permissions?: string[]
|
|
316
371
|
declaredFeatures?: FeatureDeclaration[]
|
|
372
|
+
/**
|
|
373
|
+
* Quantity limits this plugin knows how to count. Aggregated by the plugin
|
|
374
|
+
* runtime into `PluginRuntime.pluginLimits` and consumed by the access engine
|
|
375
|
+
* (useLimit / useLimitGuard) so a plugin "is born compatible" with freemium
|
|
376
|
+
* caps without any per-app wiring. See {@link LimitDeclaration}.
|
|
377
|
+
*/
|
|
378
|
+
declaredLimits?: LimitDeclaration[]
|
|
379
|
+
/** Pool RPCs this plugin ships for the server-plane agent executor. */
|
|
380
|
+
declaredRpcs?: AgentRpcDeclaration[]
|
|
381
|
+
/**
|
|
382
|
+
* Read-models this plugin exposes to the agent's generic data primitives
|
|
383
|
+
* (searchRecords/queryData) BEYOND its CRUD registries — typically its
|
|
384
|
+
* views/ledgers (v_appointments, plg_financial_movements). `key` is a stable
|
|
385
|
+
* ASCII id (never derived from a translated label); the entity's own
|
|
386
|
+
* `permission` gates each call.
|
|
387
|
+
*/
|
|
388
|
+
queryEntities?: Array<{
|
|
389
|
+
key: string
|
|
390
|
+
entity: EntityDef
|
|
391
|
+
/** Base tables may opt into createRecord; views/ledgers stay read-only. */
|
|
392
|
+
writable?: boolean
|
|
393
|
+
}>
|
|
317
394
|
registries?: PluginRegistryDef[]
|
|
318
395
|
/**
|
|
319
396
|
* Connectors this plugin contributes. An ADDON plugin declares its connector(s)
|
|
@@ -386,4 +463,6 @@ export interface PluginRuntime {
|
|
|
386
463
|
/** Connectors contributed by active plugins, grouped by the host plugin they extend. */
|
|
387
464
|
connectorsByHost: Map<string, ConnectorDefinition[]>
|
|
388
465
|
pluginFeatures: FeatureDeclaration[]
|
|
466
|
+
/** Limit declarations aggregated from every active plugin's `declaredLimits`. */
|
|
467
|
+
pluginLimits: LimitDeclaration[]
|
|
389
468
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/entity/registry.ts"],"names":[],"mappings":";;;AAgBA,IAAM,cAAA,uBAAqB,GAAA,EAA8B;AAElD,SAAS,eAAe,KAAA,EAA+B;AAC5D,EAAA,cAAA,CAAe,GAAA,CAAI,KAAA,CAAM,SAAA,EAAW,KAAK,CAAA;AAC3C;AAEO,SAAS,eAAe,GAAA,EAA2C;AACxE,EAAA,OAAO,cAAA,CAAe,IAAI,GAAG,CAAA;AAC/B;AAEO,SAAS,cAAA,GAAqC;AACnD,EAAA,OAAO,KAAA,CAAM,IAAA,CAAK,cAAA,CAAe,MAAA,EAAQ,CAAA;AAC3C;AAEO,SAAS,mBAAA,GAA4B;AAC1C,EAAA,cAAA,CAAe,KAAA,EAAM;AACvB;AAEO,SAAS,gBAAgB,SAAA,EAA8B;AAC5D,EAAA,MAAM,OAAO,SAAA,CAAU,IAAA,CAAK,aAAY,CAAE,OAAA,CAAQ,QAAQ,GAAG,CAAA;AAC7D,EAAA,IAAI,SAAA,CAAU,IAAA,EAAM,SAAA,IAAa,SAAA,CAAU,MAAM,aAAA,EAAe;AAC9D,IAAA,OAAO,GAAG,SAAA,CAAU,IAAA,CAAK,SAAS,CAAA,CAAA,EAAI,SAAA,CAAU,KAAK,aAAa,CAAA,CAAA;AAAA,EACpE;AACA,EAAA,IAAI,SAAA,CAAU,IAAA,EAAM,SAAA,EAAW,OAAO,UAAU,IAAA,CAAK,SAAA;AACrD,EAAA,OAAO,IAAA;AACT","file":"chunk-2NTHNWEH.cjs","sourcesContent":["import type { EntityDef } from '../types/crud'\nimport type { EntityArchetype } from '../types/entities'\n\nexport interface RegisteredEntity {\n entityKey: string\n label: string\n labelPlural: string\n icon?: string\n fields: EntityDef['fields']\n source: 'app' | 'plugin'\n archetype?: EntityArchetype\n pluginId?: string\n pluginName?: string\n pluginIcon?: string\n}\n\nconst entityRegistry = new Map<string, RegisteredEntity>()\n\nexport function registerEntity(entry: RegisteredEntity): void {\n entityRegistry.set(entry.entityKey, entry)\n}\n\nexport function getEntityByKey(key: string): RegisteredEntity | undefined {\n return entityRegistry.get(key)\n}\n\nexport function getAllEntities(): RegisteredEntity[] {\n return Array.from(entityRegistry.values())\n}\n\nexport function clearEntityRegistry(): void {\n entityRegistry.clear()\n}\n\nexport function deriveEntityKey(entityDef: EntityDef): string {\n const name = entityDef.name.toLowerCase().replace(/\\s+/g, '-')\n if (entityDef.data?.archetype && entityDef.data?.archetypeKind) {\n return `${entityDef.data.archetype}:${entityDef.data.archetypeKind}`\n }\n if (entityDef.data?.archetype) return entityDef.data.archetype\n return name\n}\n"]}
|