@velocms/plugin-sdk 1.0.0-alpha.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.
@@ -0,0 +1,413 @@
1
+ /**
2
+ * Hook Registry
3
+ *
4
+ * Plugins subscribe to lifecycle hooks by exporting handler functions from
5
+ * their `entry.runtime` module. The function name must match the hook name.
6
+ *
7
+ * All handlers receive a typed `HookContext` and optional payload, and must
8
+ * return a Promise<void> or Promise<HookResult>.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import type { AfterPostCreatePayload, HookContext } from "@velocms/plugin-sdk";
13
+ *
14
+ * export async function afterPostCreate(
15
+ * payload: AfterPostCreatePayload,
16
+ * ctx: HookContext
17
+ * ): Promise<void> {
18
+ * const wordCount = payload.post.content.split(/\s+/).length;
19
+ * await ctx.fetch("https://api.example.com/stats", {
20
+ * method: "POST",
21
+ * headers: { "Content-Type": "application/json" },
22
+ * body: JSON.stringify({ postId: payload.post.id, wordCount }),
23
+ * });
24
+ * }
25
+ * ```
26
+ *
27
+ * @see https://velocms.org/developers/sdk#hooks
28
+ */
29
+ /** Minimal post data provided to hooks. */
30
+ interface HookPost {
31
+ id: string;
32
+ title: string;
33
+ slug: string;
34
+ status: "draft" | "published" | "archived";
35
+ /** Rich text content (HTML string) */
36
+ content: string;
37
+ excerpt?: string;
38
+ published_at?: string;
39
+ reading_time_minutes?: number;
40
+ tags: string[];
41
+ author_id?: string;
42
+ category_id?: string;
43
+ }
44
+ /** Minimal member data provided to hooks. Email is available when `members.read_pii` capability is granted. */
45
+ interface HookMember {
46
+ id: string;
47
+ /** Available only when `members.read_pii` capability is granted */
48
+ email?: string;
49
+ /** "free" or "paid" */
50
+ tier: string;
51
+ newsletter_subscribed: boolean;
52
+ created: string;
53
+ }
54
+ interface AfterPostCreatePayload {
55
+ post: HookPost;
56
+ }
57
+ interface AfterPostUpdatePayload {
58
+ post: HookPost;
59
+ /** Field names that changed in this update */
60
+ changedFields: string[];
61
+ }
62
+ interface AfterPostDeletePayload {
63
+ postId: string;
64
+ slug: string;
65
+ }
66
+ interface BeforePostPublishPayload {
67
+ post: HookPost;
68
+ }
69
+ /** Return value from `beforePostPublish` — can augment the post. */
70
+ interface BeforePostPublishResult {
71
+ /** Suggested tags to add to the post */
72
+ suggestedTags?: string[];
73
+ /** Override the SEO title */
74
+ seoTitle?: string;
75
+ /** Override the SEO description */
76
+ seoDescription?: string;
77
+ }
78
+ interface AfterPostPublishPayload {
79
+ post: HookPost;
80
+ }
81
+ interface AfterMemberSignupPayload {
82
+ member: HookMember;
83
+ /** Source of signup: "checkout", "magic_link", "import" */
84
+ source: string;
85
+ }
86
+ interface AfterMemberUpdatePayload {
87
+ member: HookMember;
88
+ changedFields: string[];
89
+ }
90
+ interface BeforeMemberDeletePayload {
91
+ memberId: string;
92
+ }
93
+ interface PageHookData {
94
+ id: string;
95
+ title: string;
96
+ slug: string;
97
+ status: "draft" | "published";
98
+ }
99
+ interface AfterPageCreatePayload {
100
+ page: PageHookData;
101
+ }
102
+ interface AfterPageUpdatePayload {
103
+ page: PageHookData;
104
+ }
105
+ interface AfterPagePublishPayload {
106
+ page: PageHookData;
107
+ }
108
+ interface AfterOrderCreatePayload {
109
+ orderId: string;
110
+ productId: string;
111
+ memberId?: string;
112
+ amount_cents: number;
113
+ currency: string;
114
+ }
115
+ interface AfterProductCreatePayload {
116
+ productId: string;
117
+ name: string;
118
+ price_cents: number;
119
+ currency: string;
120
+ }
121
+ /** Fired when the plugin is first loaded (app start). Use for initialization. */
122
+ interface OnAppStartPayload {
123
+ /** VeloCMS version string */
124
+ velocmsVersion: string;
125
+ }
126
+ /** Fired daily by the scheduler cron. Use for periodic tasks. */
127
+ interface OnDailySchedulePayload {
128
+ /** UTC date string (e.g. "2026-04-19") */
129
+ date: string;
130
+ }
131
+ /** All available hook names. */
132
+ type HookName = "afterPostCreate" | "afterPostUpdate" | "afterPostDelete" | "beforePostPublish" | "afterPostPublish" | "afterMemberSignup" | "afterMemberUpdate" | "beforeMemberDelete" | "afterPageCreate" | "afterPageUpdate" | "afterPagePublish" | "afterOrderCreate" | "afterProductCreate" | "onAppStart" | "onDailySchedule";
133
+ /** Map from hook name to its payload type. */
134
+ interface HookPayloadMap {
135
+ afterPostCreate: AfterPostCreatePayload;
136
+ afterPostUpdate: AfterPostUpdatePayload;
137
+ afterPostDelete: AfterPostDeletePayload;
138
+ beforePostPublish: BeforePostPublishPayload;
139
+ afterPostPublish: AfterPostPublishPayload;
140
+ afterMemberSignup: AfterMemberSignupPayload;
141
+ afterMemberUpdate: AfterMemberUpdatePayload;
142
+ beforeMemberDelete: BeforeMemberDeletePayload;
143
+ afterPageCreate: AfterPageCreatePayload;
144
+ afterPageUpdate: AfterPageUpdatePayload;
145
+ afterPagePublish: AfterPagePublishPayload;
146
+ afterOrderCreate: AfterOrderCreatePayload;
147
+ afterProductCreate: AfterProductCreatePayload;
148
+ onAppStart: OnAppStartPayload;
149
+ onDailySchedule: OnDailySchedulePayload;
150
+ }
151
+
152
+ /**
153
+ * Plugin SDK — Event Bus Types
154
+ *
155
+ * Phase 2.A — Plugin SDK Event Bus
156
+ * SDK version: 1.0.0-alpha.2
157
+ *
158
+ * Exports the system event catalog and the EventBusContext interface
159
+ * that is injected as ctx.events when a plugin declares
160
+ * `capabilities.events` in its manifest.
161
+ *
162
+ * @see https://velocms.org/developers/sdk#event-bus
163
+ */
164
+
165
+ /**
166
+ * All system event names emitted by VeloCMS core.
167
+ *
168
+ * Use these as the first argument to `ctx.events.on()`.
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * ctx.events.on("post.published", async ({ post }) => {
173
+ * await notifySlack(post.title);
174
+ * });
175
+ * ```
176
+ */
177
+ type SystemEventName = "post.created" | "post.updated" | "post.published" | "post.unpublished" | "post.deleted" | "member.subscribed" | "member.unsubscribed" | "member.tier_changed" | "comment.posted" | "comment.approved" | "comment.deleted" | "page.published" | "media.uploaded";
178
+ /**
179
+ * Plugin-emitted event names follow the pattern "@org/plugin:event-name".
180
+ *
181
+ * Your org is the first segment of your plugin's `name` field in the manifest.
182
+ * Example: if `name: "@myorg/slack-notifier"`, you may emit events starting
183
+ * with "@myorg/".
184
+ */
185
+ type PluginEventName = `@${string}/${string}:${string}`;
186
+ /** Union of all valid event names: system events + plugin-namespaced events. */
187
+ type EventName = SystemEventName | PluginEventName;
188
+ /**
189
+ * Maps each SystemEventName to its payload type.
190
+ *
191
+ * This is the canonical typed catalog for handlers registered via ctx.events.on().
192
+ */
193
+ interface SystemEventPayloadMap {
194
+ "post.created": {
195
+ post: HookPost;
196
+ };
197
+ "post.updated": {
198
+ post: HookPost;
199
+ changedFields: string[];
200
+ };
201
+ "post.published": {
202
+ post: HookPost;
203
+ };
204
+ "post.unpublished": {
205
+ post: HookPost;
206
+ };
207
+ "post.deleted": {
208
+ postId: string;
209
+ slug: string;
210
+ };
211
+ "member.subscribed": {
212
+ member: HookMember;
213
+ source: string;
214
+ };
215
+ "member.unsubscribed": {
216
+ memberId: string;
217
+ };
218
+ "member.tier_changed": {
219
+ member: HookMember;
220
+ previousTier: string;
221
+ };
222
+ "comment.posted": {
223
+ commentId: string;
224
+ postId: string;
225
+ authorEmail?: string;
226
+ };
227
+ "comment.approved": {
228
+ commentId: string;
229
+ postId: string;
230
+ };
231
+ "comment.deleted": {
232
+ commentId: string;
233
+ postId: string;
234
+ };
235
+ "page.published": {
236
+ page: PageHookData;
237
+ };
238
+ "media.uploaded": {
239
+ mediaId: string;
240
+ filename: string;
241
+ mimeType: string;
242
+ };
243
+ }
244
+ /**
245
+ * The event bus context injected as `ctx.events`.
246
+ *
247
+ * Available when the plugin manifest declares:
248
+ * ```typescript
249
+ * capabilities: {
250
+ * events: {
251
+ * subscribe: ["post.published", "member.subscribed"],
252
+ * emit_custom: true, // required if you call ctx.events.emit()
253
+ * }
254
+ * }
255
+ * ```
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * export async function onActivate(ctx: HookContext) {
260
+ * ctx.events.on("post.published", async ({ post }) => {
261
+ * await ctx.fetch("https://hooks.slack.com/...", {
262
+ * method: "POST",
263
+ * body: JSON.stringify({ text: `New post: ${post.title}` }),
264
+ * });
265
+ * });
266
+ * }
267
+ * ```
268
+ */
269
+ interface EventBusContext {
270
+ /**
271
+ * Subscribe to a typed system event.
272
+ *
273
+ * @param eventName - A SystemEventName from the catalog above
274
+ * @param handler - Async handler receiving the typed payload
275
+ * @returns A function to unsubscribe this specific handler
276
+ */
277
+ on<E extends SystemEventName>(eventName: E, handler: (payload: SystemEventPayloadMap[E]) => void | Promise<void>): () => void;
278
+ /**
279
+ * Subscribe to a plugin-namespaced event.
280
+ * Used for cross-plugin communication.
281
+ *
282
+ * @param eventName - A PluginEventName in "@org/plugin:event" format
283
+ * @param handler - Handler receiving an untyped payload (validate at runtime)
284
+ * @returns A function to unsubscribe this specific handler
285
+ */
286
+ on(eventName: PluginEventName, handler: (payload: Record<string, unknown>) => void | Promise<void>): () => void;
287
+ /**
288
+ * Emit a custom event namespaced to your plugin.
289
+ *
290
+ * Requires `capabilities.events.emit_custom: true` in your manifest.
291
+ * The event_name MUST start with "@{your-org}/".
292
+ *
293
+ * @throws CapabilityError if namespace does not match manifest name
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * await ctx.events.emit("@myorg/slack-notifier:ready", {
298
+ * tenantId: ctx.tenant.id,
299
+ * });
300
+ * ```
301
+ */
302
+ emit(eventName: PluginEventName, payload: Record<string, unknown>): Promise<void>;
303
+ }
304
+ /** A generic plugin event payload — used when the event type is unknown at compile time. */
305
+ type PluginEventPayload = Record<string, unknown>;
306
+
307
+ /**
308
+ * Hook Context
309
+ *
310
+ * Every hook handler receives a `HookContext` as its second argument.
311
+ * The context provides safe, capability-enforced access to VeloCMS APIs.
312
+ *
313
+ * Access is sandboxed: attempting to use an API your plugin did not declare
314
+ * in `manifest.capabilities` will throw a `CapabilityError` at runtime.
315
+ *
316
+ * @example
317
+ * ```typescript
318
+ * export async function afterPostCreate(payload, ctx: HookContext): Promise<void> {
319
+ * // Tenant info
320
+ * const tenantId = ctx.tenant.id;
321
+ *
322
+ * // Network fetch (requires capabilities.network = true)
323
+ * const res = await ctx.fetch("https://hooks.slack.com/services/xxx/yyy/zzz", {
324
+ * method: "POST",
325
+ * body: JSON.stringify({ text: `New post: ${payload.post.title}` }),
326
+ * });
327
+ * }
328
+ * ```
329
+ *
330
+ * @see https://velocms.org/developers/sdk#context
331
+ */
332
+
333
+ /** Minimal tenant data available to plugins. */
334
+ interface ContextTenant {
335
+ id: string;
336
+ slug: string;
337
+ /** Tenant's configured locale (e.g. "en-US") */
338
+ locale: string;
339
+ }
340
+ /** Minimal site settings available to plugins. */
341
+ interface ContextSettings {
342
+ blogName: string;
343
+ siteUrl: string;
344
+ /** Active theme slug */
345
+ theme: string;
346
+ /** "free" | "pro" | "business" | "agency" */
347
+ plan: string;
348
+ }
349
+ /** Logger bound to the current plugin invocation. */
350
+ interface ContextLogger {
351
+ info(message: string, data?: Record<string, unknown>): void;
352
+ warn(message: string, data?: Record<string, unknown>): void;
353
+ error(message: string, data?: Record<string, unknown>): void;
354
+ }
355
+ /**
356
+ * The context object passed to every hook handler.
357
+ *
358
+ * All async operations in context are bound to the sandbox's CPU budget
359
+ * (default 500ms). Long-running tasks should be offloaded via `ctx.fetch`
360
+ * to an external queue.
361
+ */
362
+ interface HookContext {
363
+ /** Current tenant */
364
+ tenant: ContextTenant;
365
+ /** Site settings (non-sensitive fields only) */
366
+ settings: ContextSettings;
367
+ /**
368
+ * Scoped HTTP fetch.
369
+ *
370
+ * - Requires `capabilities.network = true`
371
+ * - URL host must be in `capabilities.network_allowlist`
372
+ * - Timeout: 5 seconds (enforced by sandbox)
373
+ * - Response body is limited to 1MB
374
+ *
375
+ * Behaves like the global `fetch()` API.
376
+ */
377
+ fetch: typeof fetch;
378
+ /** Plugin-scoped logger. Messages appear in the VeloCMS plugin activity log. */
379
+ log: ContextLogger;
380
+ /**
381
+ * Key-value storage scoped to this plugin + tenant.
382
+ * Persists across hook invocations. 1MB limit per plugin.
383
+ *
384
+ * @example
385
+ * ```typescript
386
+ * // Store a value
387
+ * await ctx.kv.set("last_run", new Date().toISOString());
388
+ *
389
+ * // Retrieve a value
390
+ * const lastRun = await ctx.kv.get("last_run");
391
+ * ```
392
+ */
393
+ kv: {
394
+ get(key: string): Promise<string | null>;
395
+ set(key: string, value: string): Promise<void>;
396
+ delete(key: string): Promise<void>;
397
+ };
398
+ /**
399
+ * Event bus — subscribe to VeloCMS system events and emit custom
400
+ * namespaced events. (Phase 2.A)
401
+ *
402
+ * Present on every `HookContext`, same as `fetch`/`kv`/`log`. As with
403
+ * those accessors, calling `.on()` / `.emit()` without declaring
404
+ * `capabilities.events` in your manifest throws a `CapabilityError` at
405
+ * runtime — the type is unconditional, the capability check happens
406
+ * when you actually use it.
407
+ *
408
+ * @see https://velocms.org/developers/sdk#event-bus
409
+ */
410
+ events: EventBusContext;
411
+ }
412
+
413
+ export type { AfterMemberSignupPayload as A, BeforeMemberDeletePayload as B, ContextLogger as C, EventBusContext as E, HookContext as H, OnAppStartPayload as O, PageHookData as P, SystemEventName as S, AfterMemberUpdatePayload as a, AfterOrderCreatePayload as b, AfterPageCreatePayload as c, AfterPagePublishPayload as d, AfterPageUpdatePayload as e, AfterPostCreatePayload as f, AfterPostDeletePayload as g, AfterPostPublishPayload as h, AfterPostUpdatePayload as i, AfterProductCreatePayload as j, BeforePostPublishPayload as k, BeforePostPublishResult as l, ContextSettings as m, ContextTenant as n, EventName as o, HookMember as p, HookName as q, HookPayloadMap as r, HookPost as s, OnDailySchedulePayload as t, PluginEventName as u, PluginEventPayload as v, SystemEventPayloadMap as w };
package/dist/index.cjs ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __copyProps = (to, from, except, desc) => {
7
+ if (from && typeof from === "object" || typeof from === "function") {
8
+ for (let key of __getOwnPropNames(from))
9
+ if (!__hasOwnProp.call(to, key) && key !== except)
10
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
11
+ }
12
+ return to;
13
+ };
14
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
15
+
16
+ // src/index.ts
17
+ var src_exports = {};
18
+ module.exports = __toCommonJS(src_exports);
19
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @velocms/plugin-sdk\n *\n * TypeScript SDK for VeloCMS plugin development.\n *\n * Provides types for capability manifests, hook registry, and context accessors.\n * Plugins built against this SDK can be submitted to the VeloCMS marketplace.\n *\n * @example\n * ```typescript\n * import type { PluginManifest, HookContext } from \"@velocms/plugin-sdk\";\n *\n * export const manifest: PluginManifest = {\n * $schema: \"https://velocms.org/schemas/plugin-v2.json\",\n * name: \"@myorg/my-plugin\",\n * displayName: \"My Plugin\",\n * version: \"1.0.0\",\n * description: \"Does something useful\",\n * author: { name: \"My Org\", email: \"plugins@myorg.com\" },\n * type: \"integration\",\n * category: \"analytics\",\n * icon: \"./icon.png\",\n * engines: { velocms: \">=1.0.0\" },\n * capabilities: { network: true },\n * pricing: { model: \"free\" },\n * entry: { runtime: \"./dist/runtime.js\" },\n * permissions_displayed_to_user: [\"Make HTTP requests to external services\"],\n * };\n * ```\n *\n * @packageDocumentation\n */\n\nexport * from \"./types/manifest\";\nexport * from \"./types/hooks\";\nexport * from \"./types/context\";\nexport * from \"./types/capabilities\";\n// Phase 2.A — Event Bus\nexport * from \"./types/events\";\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}