@velora-cms/plugin-sdk 0.9.0
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/LICENSE +202 -0
- package/README.md +27 -0
- package/dist/admin-section-registry.d.ts +4 -0
- package/dist/admin-section-registry.js +17 -0
- package/dist/built-in-serializers.d.ts +3 -0
- package/dist/built-in-serializers.js +18 -0
- package/dist/built-in-settings-schemas.d.ts +6 -0
- package/dist/built-in-settings-schemas.js +37 -0
- package/dist/built-ins.d.ts +1 -0
- package/dist/built-ins.js +142 -0
- package/dist/data-type-plugin.d.ts +21 -0
- package/dist/data-type-plugin.js +1 -0
- package/dist/datatypes-entry.d.ts +2 -0
- package/dist/datatypes-entry.js +10 -0
- package/dist/define-plugin.d.ts +25 -0
- package/dist/define-plugin.js +51 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +27 -0
- package/dist/manifest-entry.d.ts +6 -0
- package/dist/manifest-entry.js +9 -0
- package/dist/manifest-schema.d.ts +728 -0
- package/dist/manifest-schema.js +348 -0
- package/dist/manifest.d.ts +370 -0
- package/dist/manifest.js +12 -0
- package/dist/plugin-context.d.ts +30 -0
- package/dist/plugin-context.js +58 -0
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +17 -0
- package/dist/simple-storage-value-provider.d.ts +9 -0
- package/dist/simple-storage-value-provider.js +58 -0
- package/dist/structured-storage-types.d.ts +53 -0
- package/dist/structured-storage-types.js +11 -0
- package/dist/views/image-view.d.ts +3 -0
- package/dist/views/image-view.js +108 -0
- package/dist/views/rich-text-view.d.ts +2 -0
- package/dist/views/rich-text-view.js +58 -0
- package/package.json +58 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CreateDocumentTypeRequestSchema } from "@velora-cms/api-schemas";
|
|
3
|
+
// Runtime mirror of manifest.ts — the same contract, enforced for
|
|
4
|
+
// manifests that arrive as VALUES (Module Federation remotes, sideloaded
|
|
5
|
+
// plugins, the future `velora-plugin validate` CLI) where the compiler
|
|
6
|
+
// can't help. The contribution matrix is enforced by .strict() objects:
|
|
7
|
+
// each per-type contributions schema declares only its allowed keys, so
|
|
8
|
+
// a disallowed contribution fails as an unrecognized key with a clear
|
|
9
|
+
// path like `contributions.adminSections`.
|
|
10
|
+
//
|
|
11
|
+
// Lives in the SDK, not @velora-cms/api-schemas, deliberately: the manifest
|
|
12
|
+
// carries React components and functions (api-schemas must never know
|
|
13
|
+
// React), and third-party tooling needs this validator from the
|
|
14
|
+
// published package.
|
|
15
|
+
// Zod 4: z.function() is not a schema — validate callables with
|
|
16
|
+
// z.custom. The type parameter is the CONTRACT type from manifest.ts,
|
|
17
|
+
// which is what makes the compile-time sync assertion at the bottom of
|
|
18
|
+
// this file hold. React components may be functions OR objects
|
|
19
|
+
// (React.memo/forwardRef produce exotic objects).
|
|
20
|
+
const callable = (label) => z.custom((value) => typeof value === "function", { message: `${label} must be a function` });
|
|
21
|
+
const componentSchema = z.custom((value) => typeof value === "function" || (typeof value === "object" && value !== null), { message: "must be a React component" });
|
|
22
|
+
// A JSON Schema document is a plain object (DataTypePlugin.schema is the
|
|
23
|
+
// JSONSchema7 object interface, not the boolean shorthand).
|
|
24
|
+
const jsonSchemaSchema = z.custom((value) => typeof value === "object" && value !== null && !Array.isArray(value), { message: "must be a JSON Schema object" });
|
|
25
|
+
// A plugin-declared request schema must be a REAL Zod schema instance
|
|
26
|
+
// (z.object(...), z.string(), etc. — every Zod schema extends the base
|
|
27
|
+
// ZodType class), not just an object that looks like one — plugin-api-
|
|
28
|
+
// routes.ts calls .safeParse() on it directly at request time.
|
|
29
|
+
const zodSchemaField = z.instanceof(z.ZodType);
|
|
30
|
+
// Reverse-domain id per the documented contract, e.g. "com.vendor.name".
|
|
31
|
+
const pluginIdSchema = z
|
|
32
|
+
.string()
|
|
33
|
+
.regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/i, "must be a reverse-domain id like com.vendor.plugin");
|
|
34
|
+
const dialectSchema = z.enum(["postgresql", "mysql", "sqlite", "mssql", "mongodb"]);
|
|
35
|
+
const databaseCompatibilitySchema = z
|
|
36
|
+
.object({
|
|
37
|
+
storageType: z.enum(["simple", "structured"]),
|
|
38
|
+
// A plugin supporting zero databases can never be installed anywhere.
|
|
39
|
+
supported: z.array(dialectSchema).min(1),
|
|
40
|
+
unsupportedReason: z.partialRecord(dialectSchema, z.string()).optional(),
|
|
41
|
+
})
|
|
42
|
+
.strict();
|
|
43
|
+
const permissionsSchema = z
|
|
44
|
+
.object({
|
|
45
|
+
needsStructuredData: z.boolean(),
|
|
46
|
+
needsExternalNetwork: z.boolean(),
|
|
47
|
+
needsFileSystemAccess: z.boolean(),
|
|
48
|
+
})
|
|
49
|
+
.strict();
|
|
50
|
+
const pluginMigrationSchema = z
|
|
51
|
+
.object({
|
|
52
|
+
id: z.string().min(1),
|
|
53
|
+
name: z.string().min(1),
|
|
54
|
+
up: callable("up"),
|
|
55
|
+
down: callable("down"),
|
|
56
|
+
})
|
|
57
|
+
.strict();
|
|
58
|
+
const migrationHooksSchema = z
|
|
59
|
+
.object({
|
|
60
|
+
beforeMigration: callable("beforeMigration").optional(),
|
|
61
|
+
transformRow: callable("transformRow").optional(),
|
|
62
|
+
afterMigration: callable("afterMigration").optional(),
|
|
63
|
+
validateMigration: callable("validateMigration").optional(),
|
|
64
|
+
})
|
|
65
|
+
.strict();
|
|
66
|
+
const HOOK_EVENTS = [
|
|
67
|
+
"content:beforeSave",
|
|
68
|
+
"content:afterSave",
|
|
69
|
+
"content:beforePublish",
|
|
70
|
+
"content:afterPublish",
|
|
71
|
+
"content:afterTrash",
|
|
72
|
+
"content:afterRestore",
|
|
73
|
+
"content:beforeDelete",
|
|
74
|
+
"media:afterUpload",
|
|
75
|
+
"user:afterLogin",
|
|
76
|
+
"api:beforeResponse",
|
|
77
|
+
];
|
|
78
|
+
// z.partialRecord, not z.record: with enum keys, Zod 4's record is
|
|
79
|
+
// exhaustive (it would demand a handler for all ten events).
|
|
80
|
+
const hooksSchema = z.partialRecord(z.enum(HOOK_EVENTS), callable("hook handler"));
|
|
81
|
+
// The DataTypePlugin contract's structural shape — shared by manifests
|
|
82
|
+
// (dataTypes contributions arriving as data) and definePlugin (which
|
|
83
|
+
// fronts it with friendlier required-view messages).
|
|
84
|
+
export const DataTypePluginSchema = z
|
|
85
|
+
.object({
|
|
86
|
+
id: pluginIdSchema,
|
|
87
|
+
name: z.string().min(1),
|
|
88
|
+
icon: z.string().min(1),
|
|
89
|
+
version: z.string().min(1),
|
|
90
|
+
type: z.literal("datatype"),
|
|
91
|
+
schema: jsonSchemaSchema,
|
|
92
|
+
settingsSchema: jsonSchemaSchema,
|
|
93
|
+
defaultSettings: z.record(z.string(), z.unknown()),
|
|
94
|
+
views: z
|
|
95
|
+
.object({
|
|
96
|
+
input: componentSchema,
|
|
97
|
+
readOnly: componentSchema,
|
|
98
|
+
settings: componentSchema,
|
|
99
|
+
preview: componentSchema.optional(),
|
|
100
|
+
})
|
|
101
|
+
.strict(),
|
|
102
|
+
serialize: callable("serialize"),
|
|
103
|
+
deserialize: callable("deserialize"),
|
|
104
|
+
})
|
|
105
|
+
.strict();
|
|
106
|
+
const adminSectionSchema = z
|
|
107
|
+
.object({
|
|
108
|
+
id: z.string().min(1),
|
|
109
|
+
name: z.string().min(1),
|
|
110
|
+
icon: z.string().min(1),
|
|
111
|
+
view: componentSchema,
|
|
112
|
+
adminOnly: z.boolean().optional(),
|
|
113
|
+
})
|
|
114
|
+
.strict();
|
|
115
|
+
const dashboardWidgetSchema = z
|
|
116
|
+
.object({ id: z.string().min(1), name: z.string().min(1), view: componentSchema })
|
|
117
|
+
.strict();
|
|
118
|
+
const apiRouteSchema = z
|
|
119
|
+
.object({
|
|
120
|
+
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
|
|
121
|
+
path: z.string().min(1).refine((p) => p.startsWith("/"), { message: 'path must start with "/"' }),
|
|
122
|
+
access: z.enum(["admin", "apiKey", "public"]).optional(),
|
|
123
|
+
schema: z
|
|
124
|
+
.object({
|
|
125
|
+
body: zodSchemaField.optional(),
|
|
126
|
+
querystring: zodSchemaField.optional(),
|
|
127
|
+
params: zodSchemaField.optional(),
|
|
128
|
+
})
|
|
129
|
+
.strict()
|
|
130
|
+
.optional(),
|
|
131
|
+
handler: callable("handler"),
|
|
132
|
+
})
|
|
133
|
+
.strict();
|
|
134
|
+
const customFormatContributionSchema = z.object({
|
|
135
|
+
name: z.string().regex(/^[a-z0-9-]+$/),
|
|
136
|
+
build: callable("build"),
|
|
137
|
+
}).strict();
|
|
138
|
+
const serializerContributionSchema = z
|
|
139
|
+
.object({
|
|
140
|
+
extendContentResponse: callable("extendContentResponse").optional(),
|
|
141
|
+
customFormats: z.array(customFormatContributionSchema).optional(),
|
|
142
|
+
})
|
|
143
|
+
.strict()
|
|
144
|
+
.superRefine((value, ctx) => {
|
|
145
|
+
const names = (value.customFormats ?? []).map((format) => format.name);
|
|
146
|
+
if (new Set(names).size !== names.length) {
|
|
147
|
+
ctx.addIssue({ code: "custom", message: "customFormats names must be unique within a manifest" });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
const scheduledTaskSchema = z
|
|
151
|
+
.object({
|
|
152
|
+
id: z.string().min(1),
|
|
153
|
+
name: z.string().min(1),
|
|
154
|
+
cron: z.string().min(1),
|
|
155
|
+
run: callable("run"),
|
|
156
|
+
})
|
|
157
|
+
.strict();
|
|
158
|
+
const mediaProcessorSchema = z
|
|
159
|
+
.object({
|
|
160
|
+
id: z.string().min(1),
|
|
161
|
+
name: z.string().min(1),
|
|
162
|
+
process: callable("process"),
|
|
163
|
+
})
|
|
164
|
+
.strict();
|
|
165
|
+
const settingsPageSchema = z
|
|
166
|
+
.object({ id: z.string().min(1), name: z.string().min(1), view: componentSchema })
|
|
167
|
+
.strict();
|
|
168
|
+
const contentTreeActionSchema = z
|
|
169
|
+
.object({
|
|
170
|
+
id: z.string().min(1),
|
|
171
|
+
label: z.string().min(1),
|
|
172
|
+
onSelect: callable("onSelect"),
|
|
173
|
+
})
|
|
174
|
+
.strict();
|
|
175
|
+
const editorToolbarSchema = z
|
|
176
|
+
.object({
|
|
177
|
+
id: z.string().min(1),
|
|
178
|
+
label: z.string().min(1),
|
|
179
|
+
icon: z.string().min(1),
|
|
180
|
+
onClick: callable("onClick"),
|
|
181
|
+
})
|
|
182
|
+
.strict();
|
|
183
|
+
const exposedServiceSchema = z
|
|
184
|
+
.object({ id: z.string().min(1), name: z.string().min(1), service: z.unknown() })
|
|
185
|
+
.strict();
|
|
186
|
+
const bundledTemplateSchema = z
|
|
187
|
+
.object({
|
|
188
|
+
name: z.string().min(1),
|
|
189
|
+
description: z.string().optional(),
|
|
190
|
+
repository: z.string().optional(),
|
|
191
|
+
})
|
|
192
|
+
.strict();
|
|
193
|
+
// Both the record KEY (kebab-cased into the CSS custom-property name by
|
|
194
|
+
// @velora-cms/design-tokens's flatten()) and the VALUE (emitted verbatim after
|
|
195
|
+
// the colon) end up spliced into a shared, admin-wide `[data-theme] { ... }`
|
|
196
|
+
// block at runtime (see theme-css.ts's themeOverridesToCss). Braces,
|
|
197
|
+
// semicolons, and control characters are never legitimate in a single CSS
|
|
198
|
+
// identifier or value, and would otherwise let an installed theme plugin
|
|
199
|
+
// break out of its declaration and inject arbitrary rules. This is a belt
|
|
200
|
+
// to theme-css.ts's own runtime filter, enforced earlier at manifest
|
|
201
|
+
// validation time.
|
|
202
|
+
const safeCssTokenSchema = z
|
|
203
|
+
.string()
|
|
204
|
+
.max(200)
|
|
205
|
+
.regex(new RegExp('^[^{};\\u0000-\\u001f\\u007f]+$'), "must not contain CSS-escaping characters");
|
|
206
|
+
// themeTokens: minimal strict mirror of ThemeTokenOverrides — only
|
|
207
|
+
// color/radius keys exist (themes are cosmetic by construction);
|
|
208
|
+
// design-tokens has no Zod and shouldn't gain one, so token values are
|
|
209
|
+
// validated shallowly here.
|
|
210
|
+
const themeTokensSchema = z
|
|
211
|
+
.object({
|
|
212
|
+
color: z.record(safeCssTokenSchema, z.record(safeCssTokenSchema, safeCssTokenSchema)).optional(),
|
|
213
|
+
radius: z.record(safeCssTokenSchema, safeCssTokenSchema).optional(),
|
|
214
|
+
})
|
|
215
|
+
.strict();
|
|
216
|
+
// --- per-type contribution surfaces (the matrix, one .strict() each) -------
|
|
217
|
+
const dataTypeContributionsSchema = z
|
|
218
|
+
.object({
|
|
219
|
+
dataTypes: z.array(DataTypePluginSchema).optional(),
|
|
220
|
+
mediaProcessors: z.array(mediaProcessorSchema).optional(),
|
|
221
|
+
settingsPages: z.array(settingsPageSchema).optional(),
|
|
222
|
+
})
|
|
223
|
+
.strict();
|
|
224
|
+
const sectionContributionsSchema = z
|
|
225
|
+
.object({
|
|
226
|
+
dataTypes: z.array(DataTypePluginSchema).optional(),
|
|
227
|
+
adminSections: z.array(adminSectionSchema).optional(),
|
|
228
|
+
dashboardWidgets: z.array(dashboardWidgetSchema).optional(),
|
|
229
|
+
apiRoutes: z.array(apiRouteSchema).optional(),
|
|
230
|
+
hooks: hooksSchema.optional(),
|
|
231
|
+
scheduledTasks: z.array(scheduledTaskSchema).optional(),
|
|
232
|
+
settingsPages: z.array(settingsPageSchema).optional(),
|
|
233
|
+
documentTypes: z.array(CreateDocumentTypeRequestSchema).optional(),
|
|
234
|
+
bundledTemplate: bundledTemplateSchema.optional(),
|
|
235
|
+
})
|
|
236
|
+
.strict()
|
|
237
|
+
.superRefine((value, ctx) => {
|
|
238
|
+
// Two apiRoutes entries sharing a (method, path) pair within one
|
|
239
|
+
// manifest would silently last-wins in plugin-api-routes.ts's
|
|
240
|
+
// dispatcher — a validation error, not a runtime surprise, same
|
|
241
|
+
// reasoning as document-type-api.schema.ts's duplicate-field-id check.
|
|
242
|
+
const seen = new Set();
|
|
243
|
+
(value.apiRoutes ?? []).forEach((route, index) => {
|
|
244
|
+
const key = `${route.method} ${route.path}`;
|
|
245
|
+
if (seen.has(key)) {
|
|
246
|
+
ctx.addIssue({
|
|
247
|
+
code: "custom",
|
|
248
|
+
message: `Duplicate apiRoute "${key}" — method+path pairs must be unique within a manifest`,
|
|
249
|
+
path: ["apiRoutes", index],
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
seen.add(key);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
const templateContributionsSchema = z
|
|
256
|
+
.object({
|
|
257
|
+
documentTypes: z.array(CreateDocumentTypeRequestSchema).optional(),
|
|
258
|
+
})
|
|
259
|
+
.strict();
|
|
260
|
+
const themeContributionsSchema = z
|
|
261
|
+
.object({
|
|
262
|
+
themeTokens: themeTokensSchema.optional(),
|
|
263
|
+
})
|
|
264
|
+
.strict();
|
|
265
|
+
const integrationContributionsSchema = z
|
|
266
|
+
.object({
|
|
267
|
+
hooks: hooksSchema.optional(),
|
|
268
|
+
settingsPages: z.array(settingsPageSchema).optional(),
|
|
269
|
+
exposedServices: z.array(exposedServiceSchema).optional(),
|
|
270
|
+
serializers: serializerContributionSchema.optional(),
|
|
271
|
+
})
|
|
272
|
+
.strict();
|
|
273
|
+
const utilityContributionsSchema = z
|
|
274
|
+
.object({
|
|
275
|
+
dashboardWidgets: z.array(dashboardWidgetSchema).optional(),
|
|
276
|
+
hooks: hooksSchema.optional(),
|
|
277
|
+
scheduledTasks: z.array(scheduledTaskSchema).optional(),
|
|
278
|
+
mediaProcessors: z.array(mediaProcessorSchema).optional(),
|
|
279
|
+
settingsPages: z.array(settingsPageSchema).optional(),
|
|
280
|
+
contentTreeActions: z.array(contentTreeActionSchema).optional(),
|
|
281
|
+
editorToolbar: z.array(editorToolbarSchema).optional(),
|
|
282
|
+
serializers: serializerContributionSchema.optional(),
|
|
283
|
+
})
|
|
284
|
+
.strict();
|
|
285
|
+
const bundleContributionsSchema = z
|
|
286
|
+
.object({
|
|
287
|
+
bundledPlugins: z.array(pluginIdSchema).optional(),
|
|
288
|
+
bundledTemplate: bundledTemplateSchema.optional(),
|
|
289
|
+
})
|
|
290
|
+
.strict();
|
|
291
|
+
// --- the manifest -----------------------------------------------------------
|
|
292
|
+
const baseFields = {
|
|
293
|
+
id: pluginIdSchema,
|
|
294
|
+
name: z.string().min(1),
|
|
295
|
+
// Non-empty for now; strict semver checking belongs to
|
|
296
|
+
// `velora-plugin validate` (Month 7).
|
|
297
|
+
version: z.string().min(1),
|
|
298
|
+
description: z.string().min(1),
|
|
299
|
+
author: z.string().min(1),
|
|
300
|
+
license: z.string().min(1),
|
|
301
|
+
price: z.number().nonnegative().optional(),
|
|
302
|
+
// Optional by design: sideloaded/dev plugins are unsigned (Tier 2).
|
|
303
|
+
signature: z.string().optional(),
|
|
304
|
+
cmsVersion: z.string().min(1),
|
|
305
|
+
pluginDependencies: z.array(pluginIdSchema).optional(),
|
|
306
|
+
databaseCompatibility: databaseCompatibilitySchema,
|
|
307
|
+
permissions: permissionsSchema,
|
|
308
|
+
migrations: z.array(pluginMigrationSchema).optional(),
|
|
309
|
+
onInstall: callable("onInstall").optional(),
|
|
310
|
+
onUninstall: callable("onUninstall").optional(),
|
|
311
|
+
onActivate: callable("onActivate").optional(),
|
|
312
|
+
onDeactivate: callable("onDeactivate").optional(),
|
|
313
|
+
onUpdate: callable("onUpdate").optional(),
|
|
314
|
+
migrationHooks: migrationHooksSchema.optional(),
|
|
315
|
+
};
|
|
316
|
+
export const CMSPluginSchema = z.discriminatedUnion("type", [
|
|
317
|
+
z.object({ ...baseFields, type: z.literal("datatype"), contributions: dataTypeContributionsSchema }).strict(),
|
|
318
|
+
z.object({ ...baseFields, type: z.literal("section"), contributions: sectionContributionsSchema }).strict(),
|
|
319
|
+
z.object({ ...baseFields, type: z.literal("template"), contributions: templateContributionsSchema }).strict(),
|
|
320
|
+
z.object({ ...baseFields, type: z.literal("theme"), contributions: themeContributionsSchema }).strict(),
|
|
321
|
+
z
|
|
322
|
+
.object({ ...baseFields, type: z.literal("integration"), contributions: integrationContributionsSchema })
|
|
323
|
+
.strict(),
|
|
324
|
+
z.object({ ...baseFields, type: z.literal("utility"), contributions: utilityContributionsSchema }).strict(),
|
|
325
|
+
z.object({ ...baseFields, type: z.literal("bundle"), contributions: bundleContributionsSchema }).strict(),
|
|
326
|
+
]);
|
|
327
|
+
// Shared by validatePluginManifest and definePlugin — one formatting
|
|
328
|
+
// convention for every validation surface: `path: message`.
|
|
329
|
+
export function formatIssues(issues) {
|
|
330
|
+
return issues.map((issue) => {
|
|
331
|
+
const path = issue.path.join(".");
|
|
332
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Validate an untrusted value as a CMSPlugin manifest. The registry's
|
|
337
|
+
* install() (Session 62) and `velora-plugin validate` (Month 7) both go
|
|
338
|
+
* through this — errors come back as readable `path: message` strings
|
|
339
|
+
* (e.g. `contributions.adminSections: Unrecognized key ...` for a
|
|
340
|
+
* contribution-matrix violation).
|
|
341
|
+
*/
|
|
342
|
+
export function validatePluginManifest(value) {
|
|
343
|
+
const result = CMSPluginSchema.safeParse(value);
|
|
344
|
+
if (result.success) {
|
|
345
|
+
return { ok: true, manifest: result.data };
|
|
346
|
+
}
|
|
347
|
+
return { ok: false, errors: formatIssues(result.error.issues) };
|
|
348
|
+
}
|