@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VeloCMS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,216 @@
1
+ # @velocms/plugin-sdk
2
+
3
+ TypeScript SDK for VeloCMS plugin development.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @velocms/plugin-sdk --save-dev
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import type { PluginManifest, HookContext, AfterPostCreatePayload } from "@velocms/plugin-sdk";
15
+
16
+ // 1. Define your manifest
17
+ export const manifest: PluginManifest = {
18
+ $schema: "https://velocms.org/schemas/plugin-v2.json",
19
+ name: "@myorg/my-plugin",
20
+ displayName: "My Plugin",
21
+ version: "1.0.0",
22
+ description: "Sends a Slack notification on every new post.",
23
+ author: { name: "My Org", email: "plugins@myorg.com" },
24
+ type: "integration",
25
+ category: "social",
26
+ icon: "./icon.png",
27
+ engines: { velocms: ">=1.0.0" },
28
+ capabilities: {
29
+ content: { read: true },
30
+ network: true,
31
+ network_allowlist: ["hooks.slack.com"],
32
+ },
33
+ pricing: { model: "free" },
34
+ entry: { runtime: "./dist/runtime.js" },
35
+ permissions_displayed_to_user: [
36
+ "Read your posts",
37
+ "Make HTTP requests to Slack",
38
+ ],
39
+ };
40
+
41
+ // 2. Export hook handlers
42
+ export async function afterPostCreate(
43
+ payload: AfterPostCreatePayload,
44
+ ctx: HookContext
45
+ ): Promise<void> {
46
+ await ctx.fetch("https://hooks.slack.com/services/YOUR/WEBHOOK/URL", {
47
+ method: "POST",
48
+ headers: { "Content-Type": "application/json" },
49
+ body: JSON.stringify({
50
+ text: `New post published: ${payload.post.title}`,
51
+ }),
52
+ });
53
+ }
54
+ ```
55
+
56
+ ## Phase 2.A: Event Bus
57
+
58
+ SDK version `1.0.0-alpha.2` adds a real-time event bus. Plugins can subscribe to
59
+ VeloCMS system events and emit custom namespaced events.
60
+
61
+ ### Declare the capability
62
+
63
+ ```typescript
64
+ export const manifest: PluginManifest = {
65
+ // ...
66
+ capabilities: {
67
+ events: {
68
+ subscribe: ["post.published", "member.subscribed"],
69
+ emit_custom: true, // only if you call ctx.events.emit()
70
+ },
71
+ },
72
+ };
73
+ ```
74
+
75
+ ### Subscribe to system events
76
+
77
+ Register subscriptions in `onAppStart` — VeloCMS fires this hook once when
78
+ your plugin's runtime loads, which is where `ctx.events.on()` registrations
79
+ belong (registering inside a request-scoped hook like `afterPostCreate`
80
+ would re-subscribe the same handler on every invocation).
81
+
82
+ ```typescript
83
+ import type { OnAppStartPayload, HookContext } from "@velocms/plugin-sdk";
84
+
85
+ export async function onAppStart(
86
+ _payload: OnAppStartPayload,
87
+ ctx: HookContext
88
+ ): Promise<void> {
89
+ ctx.events.on("post.published", async ({ post }) => {
90
+ await ctx.fetch("https://hooks.slack.com/services/xxx/yyy/zzz", {
91
+ method: "POST",
92
+ headers: { "Content-Type": "application/json" },
93
+ body: JSON.stringify({ text: `New post: ${post.title}` }),
94
+ });
95
+ });
96
+
97
+ ctx.events.on("member.subscribed", async ({ member, source }) => {
98
+ await ctx.kv.set("last_signup_source", source);
99
+ });
100
+ }
101
+ ```
102
+
103
+ ### Emit a custom event
104
+
105
+ Custom events must be namespaced to your plugin's org prefix (`@org/`).
106
+ Other plugins can subscribe to your events by name. Emitting requires
107
+ `capabilities.events.emit_custom: true` in your manifest.
108
+
109
+ ```typescript
110
+ // Emitting (from within any hook handler that receives ctx)
111
+ await ctx.events.emit("@myorg/slack-notifier:webhook-sent", {
112
+ webhookUrl: "https://hooks.slack.com/...",
113
+ postTitle: post.title,
114
+ });
115
+
116
+ // Subscribing (from another plugin's onAppStart)
117
+ ctx.events.on("@myorg/slack-notifier:webhook-sent", async (payload) => {
118
+ await ctx.kv.set("last_slack_event", JSON.stringify(payload));
119
+ });
120
+ ```
121
+
122
+ ### Full example: Slack Notifier
123
+
124
+ A plugin is just a module exporting a `manifest` plus one function per hook
125
+ name it wants to handle — there is no `definePlugin()` wrapper. VeloCMS
126
+ looks up handlers by matching the exported function name to the `HookName`
127
+ union.
128
+
129
+ ```typescript
130
+ import type {
131
+ PluginManifest,
132
+ OnAppStartPayload,
133
+ AfterPostPublishPayload,
134
+ HookContext,
135
+ } from "@velocms/plugin-sdk";
136
+
137
+ export const manifest: PluginManifest = {
138
+ $schema: "https://velocms.org/schemas/plugin-v2.json",
139
+ name: "@myorg/slack-notifier",
140
+ displayName: "Slack Notifier",
141
+ version: "1.0.0",
142
+ description: "Posts to Slack whenever a new post is published.",
143
+ author: { name: "My Org", email: "plugins@myorg.com" },
144
+ type: "integration",
145
+ category: "social",
146
+ icon: "./icon.png",
147
+ engines: { velocms: ">=1.0.0" },
148
+ capabilities: { network: true, network_allowlist: ["hooks.slack.com"] },
149
+ pricing: { model: "free" },
150
+ entry: { runtime: "./dist/runtime.js" },
151
+ permissions_displayed_to_user: ["Make HTTP requests to Slack"],
152
+ };
153
+
154
+ export async function onAppStart(
155
+ _payload: OnAppStartPayload,
156
+ ctx: HookContext
157
+ ): Promise<void> {
158
+ ctx.log.info("Slack Notifier activated");
159
+ }
160
+
161
+ export async function afterPostPublish(
162
+ payload: AfterPostPublishPayload,
163
+ ctx: HookContext
164
+ ): Promise<void> {
165
+ const webhookUrl = await ctx.kv.get("slack_webhook_url");
166
+ if (!webhookUrl) return;
167
+ await ctx.fetch(webhookUrl, {
168
+ method: "POST",
169
+ body: JSON.stringify({ text: `New post: ${payload.post.title}` }),
170
+ });
171
+ }
172
+ ```
173
+
174
+ ### System event catalog
175
+
176
+ | Event | Payload |
177
+ |-------|---------|
178
+ | `post.created` | `{ post: HookPost }` |
179
+ | `post.updated` | `{ post: HookPost; changedFields: string[] }` |
180
+ | `post.published` | `{ post: HookPost }` |
181
+ | `post.unpublished` | `{ post: HookPost }` |
182
+ | `post.deleted` | `{ postId: string; slug: string }` |
183
+ | `member.subscribed` | `{ member: HookMember; source: string }` |
184
+ | `member.unsubscribed` | `{ memberId: string }` |
185
+ | `member.tier_changed` | `{ member: HookMember; previousTier: string }` |
186
+ | `comment.posted` | `{ commentId: string; postId: string; authorEmail?: string }` |
187
+ | `comment.approved` | `{ commentId: string; postId: string }` |
188
+ | `comment.deleted` | `{ commentId: string; postId: string }` |
189
+ | `page.published` | `{ page: PageHookData }` |
190
+ | `media.uploaded` | `{ mediaId: string; filename: string; mimeType: string }` |
191
+
192
+ ### Delivery semantics
193
+
194
+ - **Best-effort, not guaranteed.** If the Railway container restarts mid-flight,
195
+ events in transit may be missed. Plugin handlers must be idempotent.
196
+ - **5-second timeout per handler.** A hung handler is killed; the next handler still runs.
197
+ - **Circuit-breaker.** 3 consecutive handler errors → circuit opens for 5 minutes.
198
+ Events are not dispatched to a circuit-open handler. Reactivating the plugin resets the circuit.
199
+ - **30-day audit log.** All events are persisted in `plugin_events` for 30 days.
200
+ Use this for debugging via the PocketBase admin panel.
201
+
202
+ ## Documentation
203
+
204
+ Full SDK reference: [velocms.org/developers/sdk](https://velocms.org/developers/sdk)
205
+
206
+ ## Publishing to the Marketplace
207
+
208
+ 1. Build your plugin: `npm run build`
209
+ 2. Upload to [velocms.org/developers/submit](https://velocms.org/developers/submit)
210
+ 3. The automated review pipeline will scan your bundle
211
+ 4. Manual review by the VeloCMS team (2-5 business days)
212
+ 5. Published to the marketplace
213
+
214
+ ## License
215
+
216
+ MIT
@@ -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 };