@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 +21 -0
- package/README.md +216 -0
- package/dist/context-BCzNwlqR.d.cts +413 -0
- package/dist/context-BCzNwlqR.d.ts +413 -0
- package/dist/index.cjs +19 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +318 -0
- package/dist/index.d.ts +318 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/test-helpers.cjs +126 -0
- package/dist/test-helpers.cjs.map +1 -0
- package/dist/test-helpers.d.cts +110 -0
- package/dist/test-helpers.d.ts +110 -0
- package/dist/test-helpers.js +100 -0
- package/dist/test-helpers.js.map +1 -0
- package/package.json +70 -0
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 };
|