@nuxtblog/plugin-sdk 0.0.1

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/README.md ADDED
@@ -0,0 +1,308 @@
1
+ # @nuxtblog/plugin-sdk
2
+
3
+ TypeScript type definitions and base `tsconfig` for [nuxtblog](https://github.com/nuxtblog/nuxtblog) plugins.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add -D @nuxtblog/plugin-sdk
9
+ # or
10
+ npm install -D @nuxtblog/plugin-sdk
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### tsconfig.json
16
+
17
+ ```json
18
+ {
19
+ "extends": "@nuxtblog/plugin-sdk",
20
+ "include": ["src"]
21
+ }
22
+ ```
23
+
24
+ This gives you strict TypeScript compiler options and the global `nuxtblog` object with full type coverage.
25
+
26
+ ---
27
+
28
+ ## Plugin manifest (package.json)
29
+
30
+ Every plugin declares its metadata and permissions in `package.json` under the `"plugin"` field.
31
+ Capabilities must be explicitly declared — only declared APIs are injected into the plugin VM.
32
+
33
+ ```json
34
+ {
35
+ "name": "owner/my-plugin",
36
+ "version": "1.0.0",
37
+ "description": "What it does",
38
+ "author": "owner",
39
+ "license": "MIT",
40
+ "plugin": {
41
+ "title": "My Plugin",
42
+ "icon": "i-tabler-plug",
43
+ "entry": "dist/index.js",
44
+ "priority": 10,
45
+ "capabilities": {
46
+ "http": { "allow": ["hooks.slack.com"], "timeout_ms": 5000 },
47
+ "store": { "read": true, "write": true }
48
+ },
49
+ "settings": [
50
+ { "key": "webhook_url", "label": "Slack Webhook URL", "type": "string", "required": true }
51
+ ]
52
+ }
53
+ }
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Plugin API
59
+
60
+ ### `nuxtblog.filter` — synchronous interceptor
61
+
62
+ Runs **before** data is written to the database. Modify `ctx.data` or call `ctx.abort()` to cancel.
63
+ HTTP requests are **not allowed** inside filter handlers.
64
+
65
+ ```ts
66
+ // src/index.ts
67
+
68
+ nuxtblog.filter('post.create', (ctx) => {
69
+ if (!ctx.data.title) {
70
+ ctx.abort('title is required')
71
+ return
72
+ }
73
+ ctx.data.title = ctx.data.title.trim()
74
+ // ctx.next() is optional — the chain continues unless abort() is called
75
+ })
76
+
77
+ nuxtblog.filter('content.render', (ctx) => {
78
+ // ctx.input — read-only snapshot before the chain (for diff/audit)
79
+ // ctx.data — mutable, changes are returned to the caller
80
+ // ctx.meta — shared KV across all plugins in the same chain
81
+ ctx.data.content = ctx.data.content.replace(/\bfoo\b/g, 'bar')
82
+ })
83
+ ```
84
+
85
+ ### `nuxtblog.on` — async event handler
86
+
87
+ Runs **after** the operation completes. HTTP requests are allowed.
88
+
89
+ ```ts
90
+ nuxtblog.on('post.published', (data) => {
91
+ const url = nuxtblog.settings.get('webhook_url') as string
92
+ nuxtblog.http.fetch(url, {
93
+ method: 'POST',
94
+ body: { text: `New post: ${data.title}` },
95
+ })
96
+ })
97
+
98
+ nuxtblog.on('user.registered', (data) => {
99
+ nuxtblog.log.info(`New user: ${data.username} (${data.email})`)
100
+ })
101
+ ```
102
+
103
+ ### `nuxtblog.http.fetch` — synchronous HTTP
104
+
105
+ Available when `capabilities.http` is declared. Returns immediately (not a Promise).
106
+ Blocked inside `filter` handlers.
107
+
108
+ ```ts
109
+ const res = nuxtblog.http.fetch<{ id: string }>('https://api.example.com/create', {
110
+ method: 'POST',
111
+ body: { title: 'Hello' },
112
+ headers: { Authorization: 'Bearer token' },
113
+ })
114
+
115
+ if (res.ok) {
116
+ nuxtblog.log.info('created: ' + res.body.id)
117
+ } else {
118
+ nuxtblog.log.error(res.error ?? `HTTP ${res.status}`)
119
+ }
120
+ ```
121
+
122
+ ### `nuxtblog.store` — persistent key-value store
123
+
124
+ Available when `capabilities.store` is declared. Keys are namespaced per plugin.
125
+
126
+ ```ts
127
+ nuxtblog.store.set('last_run', new Date().toISOString())
128
+ const last = nuxtblog.store.get('last_run') // unknown
129
+ nuxtblog.store.delete('last_run')
130
+ ```
131
+
132
+ ### `nuxtblog.settings.get` — admin-configured settings
133
+
134
+ Always available. Cached for 30 seconds.
135
+
136
+ ```ts
137
+ const apiKey = nuxtblog.settings.get('api_key') as string
138
+ ```
139
+
140
+ ### `nuxtblog.log` — server logging
141
+
142
+ ```ts
143
+ nuxtblog.log.info('hello')
144
+ nuxtblog.log.warn('something looks off')
145
+ nuxtblog.log.error('this failed')
146
+ nuxtblog.log.debug('verbose details')
147
+ ```
148
+
149
+ ---
150
+
151
+ ## Declarative webhooks (no JS needed)
152
+
153
+ Simple outbound notifications can be declared in the manifest instead of writing JS.
154
+
155
+ **Never hardcode secrets in the manifest.** Use `{{settings.key}}` placeholders in `url`
156
+ and header values — they are resolved at dispatch time from admin-configured settings:
157
+
158
+ ```json
159
+ {
160
+ "plugin": {
161
+ "settings": [
162
+ { "key": "webhook_url", "label": "Webhook URL", "type": "string", "placeholder": "https://hooks.slack.com/..." },
163
+ { "key": "webhook_token", "label": "Webhook Token", "type": "password", "placeholder": "xoxb-..." }
164
+ ],
165
+ "webhooks": [
166
+ {
167
+ "url": "{{settings.webhook_url}}",
168
+ "events": ["post.published", "comment.created"],
169
+ "headers": { "Authorization": "Bearer {{settings.webhook_token}}" }
170
+ }
171
+ ]
172
+ }
173
+ }
174
+ ```
175
+
176
+ Event patterns: `"post.*"` matches all post events; `"*"` matches everything.
177
+
178
+ ---
179
+
180
+ ## Declarative pipelines (multi-step async workflows)
181
+
182
+ For multi-step workflows with conditionals and retries, declare a pipeline in the manifest.
183
+ JS functions exported at module scope are called by name.
184
+
185
+ ```json
186
+ {
187
+ "plugin": {
188
+ "capabilities": {
189
+ "http": { "allow": ["ai-api.example.com", "hooks.slack.com"] }
190
+ },
191
+ "pipelines": [
192
+ {
193
+ "name": "post-publish",
194
+ "trigger": "post.published",
195
+ "steps": [
196
+ {
197
+ "type": "js",
198
+ "name": "Generate summary",
199
+ "fn": "generateSummary",
200
+ "timeout_ms": 8000,
201
+ "retry": 1
202
+ },
203
+ {
204
+ "type": "condition",
205
+ "name": "Branch by category",
206
+ "if": "ctx.data.category === 'tech'",
207
+ "then": [
208
+ { "type": "webhook", "name": "Post to Twitter", "url": "https://api.twitter.com/..." }
209
+ ],
210
+ "else": [
211
+ { "type": "js", "name": "Notify Slack", "fn": "notifySlack" }
212
+ ]
213
+ }
214
+ ]
215
+ }
216
+ ]
217
+ }
218
+ }
219
+ ```
220
+
221
+ ```ts
222
+ // src/index.ts — functions called by pipeline steps must be exported at module scope
223
+
224
+ function generateSummary(ctx: StepContext) {
225
+ const res = nuxtblog.http.fetch<{ summary: string }>('https://ai-api.example.com/summarize', {
226
+ method: 'POST',
227
+ body: { content: ctx.data.content as string },
228
+ })
229
+ if (res.ok) {
230
+ ctx.data.excerpt = res.body.summary
231
+ }
232
+ }
233
+
234
+ function notifySlack(ctx: StepContext) {
235
+ nuxtblog.http.fetch('https://hooks.slack.com/services/xxx', {
236
+ method: 'POST',
237
+ body: { text: `New post: ${ctx.data.title}` },
238
+ })
239
+ }
240
+ ```
241
+
242
+ Step types:
243
+ - `"js"` — call an exported JS function; supports `timeout_ms` and `retry`
244
+ - `"webhook"` — POST the event payload to a URL; supports `timeout_ms` and `retry`
245
+ - `"condition"` — evaluate a JS boolean expression, branch to `then` or `else`
246
+
247
+ Retry backoff: 200 ms → 400 ms → 800 ms … capped at 8 s.
248
+
249
+ ---
250
+
251
+ ## Event reference
252
+
253
+ ### Fire-and-forget events (`nuxtblog.on`)
254
+
255
+ | Event | Payload type |
256
+ |-------|-------------|
257
+ | `post.created` | `PostCreatedPayload` |
258
+ | `post.updated` | `PostUpdatedPayload` |
259
+ | `post.published` | `PostPublishedPayload` |
260
+ | `post.deleted` | `PostDeletedPayload` |
261
+ | `post.viewed` | `PostViewedPayload` |
262
+ | `comment.created` | `CommentCreatedPayload` |
263
+ | `comment.deleted` | `CommentDeletedPayload` |
264
+ | `comment.status_changed` | `CommentStatusChangedPayload` |
265
+ | `comment.approved` | `CommentApprovedPayload` |
266
+ | `user.registered` | `UserRegisteredPayload` |
267
+ | `user.updated` | `UserUpdatedPayload` |
268
+ | `user.deleted` | `UserDeletedPayload` |
269
+ | `user.followed` | `UserFollowedPayload` |
270
+ | `user.login` | `UserLoginPayload` |
271
+ | `user.logout` | `UserLogoutPayload` |
272
+ | `media.uploaded` | `MediaUploadedPayload` |
273
+ | `media.deleted` | `MediaDeletedPayload` |
274
+ | `taxonomy.created` | `TaxonomyCreatedPayload` |
275
+ | `taxonomy.deleted` | `TaxonomyDeletedPayload` |
276
+ | `term.created` | `TermCreatedPayload` |
277
+ | `term.deleted` | `TermDeletedPayload` |
278
+ | `reaction.added` | `ReactionPayload` |
279
+ | `reaction.removed` | `ReactionPayload` |
280
+ | `checkin.done` | `CheckinPayload` |
281
+ | `option.updated` | `OptionUpdatedPayload` |
282
+ | `plugin.installed` | `PluginInstalledPayload` |
283
+ | `plugin.uninstalled` | `PluginUninstalledPayload` |
284
+
285
+ ### Filter events (`nuxtblog.filter`)
286
+
287
+ | Event | `ctx.data` type | Notes |
288
+ |-------|----------------|-------|
289
+ | `post.create` | `FilterPostCreateData` | |
290
+ | `post.update` | `FilterPostUpdateData` | Only updated fields are present |
291
+ | `post.delete` | `FilterPostDeleteData` | `abort()` cancels the deletion |
292
+ | `comment.create` | `FilterCommentCreateData` | |
293
+ | `comment.delete` | `FilterCommentDeleteData` | `abort()` cancels the deletion |
294
+ | `term.create` | `FilterTermCreateData` | |
295
+ | `user.register` | `FilterUserRegisterData` | |
296
+ | `user.update` | `FilterUserUpdateData` | Only updated fields are present |
297
+ | `media.upload` | `FilterMediaUploadData` | |
298
+ | `content.render` | `FilterContentRenderData` | Modify `ctx.data.content` to change what readers see |
299
+
300
+ ---
301
+
302
+ ## Publishing a plugin
303
+
304
+ See the [nuxtblog plugin registry](https://github.com/nuxtblog/registry) for how to submit your plugin to the marketplace.
305
+
306
+ ## License
307
+
308
+ MIT
package/index.d.ts ADDED
@@ -0,0 +1,642 @@
1
+ /**
2
+ * @nuxtblog/plugin-sdk — Type declarations for nuxtblog plugins
3
+ *
4
+ * ─── Manifest (package.json "plugin" field) ──────────────────────────────────
5
+ *
6
+ * {
7
+ * "name": "owner/repo", // required, unique plugin ID
8
+ * "title": "My Plugin",
9
+ * "description": "...",
10
+ * "version": "1.0.0",
11
+ * "author": "owner",
12
+ * "icon": "i-tabler-plug", // any Tabler / Lucide icon name
13
+ * "entry": "dist/index.js", // bundled JS entry (default: dist/index.js)
14
+ * "css": ".my-class { ... }", // optional CSS injected into frontend <head>
15
+ * "priority": 10, // execution order: lower runs first (default: 10)
16
+ * "settings": [ ... ], // see SettingField below
17
+ * "capabilities": { // required — only declared APIs are injected
18
+ * "http": { "allow": ["api.example.com"], "timeout_ms": 5000 },
19
+ * "store": { "read": true, "write": true },
20
+ * "events": { "subscribe": ["post.*"] }
21
+ * },
22
+ * "webhooks": [
23
+ * // Declarative outbound webhooks — no JS needed.
24
+ * // url and header values support {{settings.key}} to read admin-configured values
25
+ * // at dispatch time (30-second TTL cache). Never hardcode secrets in the manifest.
26
+ * {
27
+ * "url": "{{settings.webhook_url}}",
28
+ * "events": ["post.*"],
29
+ * "headers": { "Authorization": "Bearer {{settings.webhook_token}}" }
30
+ * }
31
+ * ],
32
+ * "pipelines": [ // declarative multi-step async workflows
33
+ * {
34
+ * "name": "post-publish",
35
+ * "trigger": "post.published",
36
+ * "steps": [
37
+ * { "type": "js", "name": "Generate summary", "fn": "generateSummary", "timeout_ms": 8000, "retry": 1 },
38
+ * {
39
+ * // webhook step url also supports {{settings.key}}
40
+ * "type": "webhook", "name": "Notify Slack",
41
+ * "url": "{{settings.slack_webhook_url}}", "headers": {}
42
+ * },
43
+ * {
44
+ * "type": "condition", "name": "Branch by category",
45
+ * "if": "ctx.data.category === 'tech'",
46
+ * "then": [ { "type": "js", "name": "Tweet it", "fn": "postTweet" } ],
47
+ * "else": [ { "type": "js", "name": "Log skip", "fn": "logSkip" } ]
48
+ * }
49
+ * ]
50
+ * }
51
+ * ]
52
+ * }
53
+ *
54
+ * ─── Plugin API overview ─────────────────────────────────────────────────────
55
+ *
56
+ * nuxtblog.filter(event, (ctx) => { }) ← guard: sync, can abort, HTTP blocked
57
+ * nuxtblog.on(event, (data) => { }) ← simple response: async, HTTP allowed
58
+ * function myStep(ctx: StepContext) { } ← pipeline step: async, HTTP allowed
59
+ * manifest.webhooks ← outbound POST, no JS needed
60
+ * manifest.pipelines ← multi-step workflow with retry/condition
61
+ */
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Filter context (nuxtblog.filter handlers)
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /**
68
+ * Context passed to every `nuxtblog.filter()` handler.
69
+ *
70
+ * @example
71
+ * nuxtblog.filter('post.create', (ctx) => {
72
+ * if (ctx.data.title.length > 200) {
73
+ * ctx.abort('title too long')
74
+ * return
75
+ * }
76
+ * ctx.data.title = ctx.data.title.trim()
77
+ * ctx.meta.slug = ctx.data.title.toLowerCase().replace(/\s+/g, '-')
78
+ * ctx.next() // optional — chain continues even without it
79
+ * })
80
+ */
81
+ interface PluginCtx<T extends Record<string, unknown> = Record<string, unknown>> {
82
+ /** The filter event name (e.g. "post.create"). */
83
+ readonly event: string
84
+ /**
85
+ * Read-only snapshot of `data` taken before the filter chain started.
86
+ * Use it for audit / diff purposes; modifications have no effect.
87
+ */
88
+ readonly input: Readonly<T>
89
+ /** Mutable payload. Modify fields here — they are persisted after the chain. */
90
+ data: T
91
+ /** Request-scoped KV store shared across all plugins in the same filter chain. */
92
+ meta: Record<string, unknown>
93
+ /**
94
+ * Signal that this handler is done and the chain should continue.
95
+ * Calling `next()` is optional — the chain always continues unless `abort()` is called.
96
+ */
97
+ next(): void
98
+ /**
99
+ * Stop the filter chain immediately. All subsequent handlers are skipped.
100
+ * The operation is rejected and `reason` is surfaced to the caller.
101
+ */
102
+ abort(reason: string): void
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Pipeline step context (pipeline "type": "js" step handlers)
107
+ // ---------------------------------------------------------------------------
108
+
109
+ /**
110
+ * Context passed to functions called as pipeline JS steps.
111
+ * Unlike `PluginCtx`, this context:
112
+ * - lives for the entire pipeline (not just one handler)
113
+ * - allows `nuxtblog.http.fetch` calls
114
+ * - has no `input` snapshot or `next()` method
115
+ *
116
+ * Export the function at module scope — the pipeline engine looks it up by name.
117
+ *
118
+ * @example
119
+ * // In package.json pipeline step: { "type": "js", "fn": "generateSummary" }
120
+ * function generateSummary(ctx: StepContext) {
121
+ * const res = nuxtblog.http.fetch('https://ai-api/summarize', {
122
+ * method: 'POST', body: { content: ctx.data.content as string }
123
+ * })
124
+ * ctx.data.excerpt = (res.body as any).summary
125
+ * }
126
+ */
127
+ interface StepContext<T extends Record<string, unknown> = Record<string, unknown>> {
128
+ /** The trigger event name (e.g. "post.published"). */
129
+ readonly event: string
130
+ /** Mutable payload shared across all steps in the pipeline. */
131
+ data: T
132
+ /** Pipeline-scoped KV store; values written by earlier steps are visible to later ones. */
133
+ meta: Record<string, unknown>
134
+ /** Stop the pipeline immediately. Subsequent steps are skipped. */
135
+ abort(reason: string): void
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Event payloads (nuxtblog.on)
140
+ // ---------------------------------------------------------------------------
141
+
142
+ // ── Post ────────────────────────────────────────────────────────────────────
143
+
144
+ interface PostCreatedPayload {
145
+ id: number
146
+ title: string
147
+ slug: string
148
+ excerpt: string
149
+ /** 0 = post 1 = page */
150
+ post_type: number
151
+ author_id: number
152
+ /** 0 = draft 1 = published 2 = trash */
153
+ status: number
154
+ }
155
+
156
+ interface PostUpdatedPayload {
157
+ id: number
158
+ title: string
159
+ slug: string
160
+ excerpt: string
161
+ post_type: number
162
+ author_id: number
163
+ status: number
164
+ }
165
+
166
+ interface PostPublishedPayload {
167
+ id: number
168
+ title: string
169
+ slug: string
170
+ excerpt: string
171
+ post_type: number
172
+ author_id: number
173
+ }
174
+
175
+ interface PostDeletedPayload {
176
+ id: number
177
+ title: string
178
+ slug: string
179
+ post_type: number
180
+ author_id: number
181
+ }
182
+
183
+ interface PostViewedPayload {
184
+ id: number
185
+ user_id: number
186
+ }
187
+
188
+ // ── Comment ──────────────────────────────────────────────────────────────────
189
+
190
+ interface CommentCreatedPayload {
191
+ id: number
192
+ /** 0 = pending 1 = approved 2 = spam */
193
+ status: number
194
+ object_type: string
195
+ object_id: number
196
+ object_title: string
197
+ object_slug: string
198
+ post_author_id: number
199
+ /** Undefined for top-level comments */
200
+ parent_id?: number
201
+ parent_author_id: number
202
+ author_id: number
203
+ author_name: string
204
+ author_email: string
205
+ content: string
206
+ }
207
+
208
+ interface CommentDeletedPayload {
209
+ id: number
210
+ object_type: string
211
+ object_id: number
212
+ }
213
+
214
+ interface CommentStatusChangedPayload {
215
+ id: number
216
+ object_type: string
217
+ object_id: number
218
+ /** 0 = pending 1 = approved 2 = spam */
219
+ old_status: number
220
+ new_status: number
221
+ moderator_id: number
222
+ }
223
+
224
+ interface CommentApprovedPayload {
225
+ id: number
226
+ object_type: string
227
+ object_id: number
228
+ moderator_id: number
229
+ }
230
+
231
+ // ── User ─────────────────────────────────────────────────────────────────────
232
+
233
+ interface UserRegisteredPayload {
234
+ id: number
235
+ username: string
236
+ email: string
237
+ display_name: string
238
+ locale: string
239
+ /** 0 = subscriber 1 = contributor 2 = editor 3 = admin */
240
+ role: number
241
+ }
242
+
243
+ interface UserUpdatedPayload {
244
+ id: number
245
+ username: string
246
+ email: string
247
+ display_name: string
248
+ locale: string
249
+ role: number
250
+ /** 0 = active 1 = inactive/banned */
251
+ status: number
252
+ }
253
+
254
+ interface UserDeletedPayload {
255
+ id: number
256
+ username: string
257
+ email: string
258
+ }
259
+
260
+ interface UserFollowedPayload {
261
+ follower_id: number
262
+ follower_name: string
263
+ follower_avatar: string
264
+ following_id: number
265
+ }
266
+
267
+ interface UserLoginPayload {
268
+ id: number
269
+ username: string
270
+ email: string
271
+ role: number
272
+ }
273
+
274
+ interface UserLogoutPayload {
275
+ id: number
276
+ }
277
+
278
+ // ── Media ─────────────────────────────────────────────────────────────────────
279
+
280
+ interface MediaUploadedPayload {
281
+ id: number
282
+ uploader_id: number
283
+ filename: string
284
+ mime_type: string
285
+ /** bytes */
286
+ file_size: number
287
+ /** public CDN / storage URL */
288
+ url: string
289
+ category: string
290
+ /** 0 for non-image files */
291
+ width: number
292
+ height: number
293
+ }
294
+
295
+ interface MediaDeletedPayload {
296
+ id: number
297
+ uploader_id: number
298
+ filename: string
299
+ mime_type: string
300
+ category: string
301
+ }
302
+
303
+ // ── Taxonomy / Term ───────────────────────────────────────────────────────────
304
+
305
+ interface TaxonomyCreatedPayload {
306
+ id: number
307
+ term_id: number
308
+ term_name: string
309
+ term_slug: string
310
+ /** e.g. "category" | "tag" */
311
+ taxonomy: string
312
+ }
313
+
314
+ interface TaxonomyDeletedPayload {
315
+ id: number
316
+ term_name: string
317
+ term_slug: string
318
+ taxonomy: string
319
+ }
320
+
321
+ interface TermCreatedPayload {
322
+ id: number
323
+ name: string
324
+ slug: string
325
+ }
326
+
327
+ interface TermDeletedPayload {
328
+ id: number
329
+ name: string
330
+ slug: string
331
+ }
332
+
333
+ // ── Reaction / Checkin ────────────────────────────────────────────────────────
334
+
335
+ interface ReactionPayload {
336
+ user_id: number
337
+ object_type: string
338
+ object_id: number
339
+ type: "like" | "bookmark"
340
+ }
341
+
342
+ interface CheckinPayload {
343
+ user_id: number
344
+ streak: number
345
+ already_checked_in: boolean
346
+ }
347
+
348
+ // ── System ────────────────────────────────────────────────────────────────────
349
+
350
+ interface OptionUpdatedPayload {
351
+ key: string
352
+ value: unknown
353
+ }
354
+
355
+ interface PluginInstalledPayload {
356
+ id: string
357
+ title: string
358
+ version: string
359
+ author: string
360
+ }
361
+
362
+ interface PluginUninstalledPayload {
363
+ id: string
364
+ }
365
+
366
+ // ---------------------------------------------------------------------------
367
+ // Filter data shapes (nuxtblog.filter — ctx.data type)
368
+ // ---------------------------------------------------------------------------
369
+
370
+ interface FilterPostCreateData {
371
+ title: string
372
+ slug: string
373
+ content: string
374
+ excerpt: string
375
+ /** 0 = draft 1 = published */
376
+ status: number
377
+ [key: string]: unknown
378
+ }
379
+
380
+ /** Only fields being updated are present */
381
+ type FilterPostUpdateData = Partial<FilterPostCreateData>
382
+
383
+ interface FilterPostDeleteData {
384
+ id: number
385
+ [key: string]: unknown
386
+ }
387
+
388
+ interface FilterCommentCreateData {
389
+ content: string
390
+ author_name: string
391
+ author_email: string
392
+ [key: string]: unknown
393
+ }
394
+
395
+ interface FilterCommentDeleteData {
396
+ id: number
397
+ [key: string]: unknown
398
+ }
399
+
400
+ interface FilterTermCreateData {
401
+ name: string
402
+ slug: string
403
+ [key: string]: unknown
404
+ }
405
+
406
+ interface FilterUserRegisterData {
407
+ username: string
408
+ email: string
409
+ display_name: string
410
+ [key: string]: unknown
411
+ }
412
+
413
+ /** Only fields being updated are present */
414
+ interface FilterUserUpdateData {
415
+ display_name?: string
416
+ bio?: string
417
+ locale?: string
418
+ status?: number
419
+ [key: string]: unknown
420
+ }
421
+
422
+ interface FilterMediaUploadData {
423
+ filename: string
424
+ mime_type: string
425
+ category: string
426
+ alt_text: string
427
+ title: string
428
+ [key: string]: unknown
429
+ }
430
+
431
+ /**
432
+ * Fired when a post, page, or doc is about to be returned to the frontend.
433
+ * Modify `ctx.data.content` to change what the reader sees.
434
+ */
435
+ interface FilterContentRenderData {
436
+ /** Raw markdown source */
437
+ content: string
438
+ /** "post" | "page" | "doc" */
439
+ type: string
440
+ id: number
441
+ slug: string
442
+ title: string
443
+ [key: string]: unknown
444
+ }
445
+
446
+ // ---------------------------------------------------------------------------
447
+ // nuxtblog.http
448
+ // ---------------------------------------------------------------------------
449
+
450
+ interface FetchOptions {
451
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
452
+ /** Plain object is JSON-serialised automatically; string is sent as-is. */
453
+ body?: Record<string, unknown> | string
454
+ headers?: Record<string, string>
455
+ }
456
+
457
+ interface FetchResult<T = unknown> {
458
+ /** true when HTTP status is 200–299 */
459
+ ok: boolean
460
+ status: number
461
+ /** Auto JSON.parse'd; falls back to raw string on parse failure. */
462
+ body: T
463
+ /** Present when the request itself failed (network error, timeout, domain blocked, etc.). */
464
+ error?: string
465
+ }
466
+
467
+ // ---------------------------------------------------------------------------
468
+ // nuxtblog.store
469
+ // ---------------------------------------------------------------------------
470
+
471
+ interface BlogStore {
472
+ /** Returns the stored value for `key`, or `null` if not set. */
473
+ get(key: string): unknown
474
+ /** Persists `value` under `key`. Value is JSON-serialised. */
475
+ set(key: string, value: unknown): void
476
+ /** Removes the entry for `key`. */
477
+ delete(key: string): void
478
+ }
479
+
480
+ // ---------------------------------------------------------------------------
481
+ // nuxtblog.on — typed overloads
482
+ // ---------------------------------------------------------------------------
483
+
484
+ interface BlogOn {
485
+ // Post
486
+ (event: "post.created", handler: (payload: PostCreatedPayload) => void): void
487
+ (event: "post.updated", handler: (payload: PostUpdatedPayload) => void): void
488
+ (event: "post.published", handler: (payload: PostPublishedPayload) => void): void
489
+ (event: "post.deleted", handler: (payload: PostDeletedPayload) => void): void
490
+ (event: "post.viewed", handler: (payload: PostViewedPayload) => void): void
491
+ // Comment
492
+ (event: "comment.created", handler: (payload: CommentCreatedPayload) => void): void
493
+ (event: "comment.deleted", handler: (payload: CommentDeletedPayload) => void): void
494
+ (event: "comment.status_changed", handler: (payload: CommentStatusChangedPayload) => void): void
495
+ (event: "comment.approved", handler: (payload: CommentApprovedPayload) => void): void
496
+ // User
497
+ (event: "user.registered", handler: (payload: UserRegisteredPayload) => void): void
498
+ (event: "user.updated", handler: (payload: UserUpdatedPayload) => void): void
499
+ (event: "user.deleted", handler: (payload: UserDeletedPayload) => void): void
500
+ (event: "user.followed", handler: (payload: UserFollowedPayload) => void): void
501
+ (event: "user.login", handler: (payload: UserLoginPayload) => void): void
502
+ (event: "user.logout", handler: (payload: UserLogoutPayload) => void): void
503
+ // Media
504
+ (event: "media.uploaded", handler: (payload: MediaUploadedPayload) => void): void
505
+ (event: "media.deleted", handler: (payload: MediaDeletedPayload) => void): void
506
+ // Taxonomy / Term
507
+ (event: "taxonomy.created", handler: (payload: TaxonomyCreatedPayload) => void): void
508
+ (event: "taxonomy.deleted", handler: (payload: TaxonomyDeletedPayload) => void): void
509
+ (event: "term.created", handler: (payload: TermCreatedPayload) => void): void
510
+ (event: "term.deleted", handler: (payload: TermDeletedPayload) => void): void
511
+ // Reaction / Checkin
512
+ (event: "reaction.added", handler: (payload: ReactionPayload) => void): void
513
+ (event: "reaction.removed", handler: (payload: ReactionPayload) => void): void
514
+ (event: "checkin.done", handler: (payload: CheckinPayload) => void): void
515
+ // System
516
+ (event: "option.updated", handler: (payload: OptionUpdatedPayload) => void): void
517
+ (event: "plugin.installed", handler: (payload: PluginInstalledPayload) => void): void
518
+ (event: "plugin.uninstalled", handler: (payload: PluginUninstalledPayload) => void): void
519
+ /** Fallback for custom / future events */
520
+ (event: string, handler: (payload: unknown) => void): void
521
+ }
522
+
523
+ // ---------------------------------------------------------------------------
524
+ // nuxtblog.filter — typed overloads
525
+ //
526
+ // Handlers receive a PluginCtx. Modify ctx.data to change the data that will
527
+ // be persisted. Call ctx.abort(reason) to reject the operation entirely.
528
+ // Calling ctx.next() is optional — the chain continues unless abort() is called.
529
+ // ---------------------------------------------------------------------------
530
+
531
+ interface BlogFilter {
532
+ (event: "post.create",
533
+ handler: (ctx: PluginCtx<FilterPostCreateData>) => void): void
534
+ (event: "post.update",
535
+ handler: (ctx: PluginCtx<FilterPostUpdateData>) => void): void
536
+ (event: "post.delete",
537
+ handler: (ctx: PluginCtx<FilterPostDeleteData>) => void): void
538
+ (event: "comment.create",
539
+ handler: (ctx: PluginCtx<FilterCommentCreateData>) => void): void
540
+ (event: "comment.delete",
541
+ handler: (ctx: PluginCtx<FilterCommentDeleteData>) => void): void
542
+ (event: "term.create",
543
+ handler: (ctx: PluginCtx<FilterTermCreateData>) => void): void
544
+ (event: "user.register",
545
+ handler: (ctx: PluginCtx<FilterUserRegisterData>) => void): void
546
+ (event: "user.update",
547
+ handler: (ctx: PluginCtx<FilterUserUpdateData>) => void): void
548
+ (event: "media.upload",
549
+ handler: (ctx: PluginCtx<FilterMediaUploadData>) => void): void
550
+ (event: "content.render",
551
+ handler: (ctx: PluginCtx<FilterContentRenderData>) => void): void
552
+ /** Fallback for custom / future filter events */
553
+ (event: string, handler: (ctx: PluginCtx) => void): void
554
+ }
555
+
556
+ // ---------------------------------------------------------------------------
557
+ // Global nuxtblog object
558
+ // ---------------------------------------------------------------------------
559
+
560
+ declare const nuxtblog: {
561
+ /**
562
+ * Subscribe to a fire-and-forget event (async, runs after the operation completes).
563
+ * HTTP requests are allowed. Cannot modify or cancel the triggering operation.
564
+ *
565
+ * @example
566
+ * nuxtblog.on('post.published', (data) => {
567
+ * nuxtblog.http.fetch('https://hooks.slack.com/...', {
568
+ * method: 'POST', body: { text: 'New post: ' + data.title }
569
+ * })
570
+ * })
571
+ */
572
+ on: BlogOn
573
+
574
+ /**
575
+ * Register a synchronous data interceptor.
576
+ * Runs BEFORE data is written to the database.
577
+ * Modify `ctx.data` to change the persisted values.
578
+ * Call `ctx.abort(reason)` to reject the operation entirely.
579
+ *
580
+ * ⚠️ HTTP requests (`nuxtblog.http.fetch`) are NOT allowed inside filter handlers.
581
+ * Use `nuxtblog.on` or a pipeline step for async side-effects.
582
+ *
583
+ * @example
584
+ * nuxtblog.filter('post.create', (ctx) => {
585
+ * if (!ctx.data.title) {
586
+ * ctx.abort('title is required')
587
+ * return
588
+ * }
589
+ * ctx.data.title = ctx.data.title.trim()
590
+ * })
591
+ */
592
+ filter: BlogFilter
593
+
594
+ /** Write a message to the server log (prefixed with [plugin:<id>]). */
595
+ log: {
596
+ info(message: string): void
597
+ warn(message: string): void
598
+ error(message: string): void
599
+ debug(message: string): void
600
+ }
601
+
602
+ /**
603
+ * Synchronous HTTP client. Available when the plugin declares `capabilities.http`.
604
+ *
605
+ * ⚠️ Blocked inside `nuxtblog.filter` handlers regardless of capability declarations.
606
+ * Safe to use in `nuxtblog.on` handlers and pipeline JS steps.
607
+ */
608
+ http: {
609
+ /**
610
+ * Make a synchronous HTTP request. Default timeout: 15 seconds (overridable
611
+ * via `capabilities.http.timeout_ms` in the manifest).
612
+ * Returns immediately with the result (not a Promise).
613
+ *
614
+ * @example
615
+ * const res = nuxtblog.http.fetch<{ id: number }>('https://api.example.com/notify', {
616
+ * method: 'POST',
617
+ * body: { message: 'hello' },
618
+ * headers: { Authorization: 'Bearer token' },
619
+ * })
620
+ * if (res.ok) nuxtblog.log.info('notified: ' + res.body.id)
621
+ */
622
+ fetch<T = unknown>(url: string, opts?: FetchOptions): FetchResult<T>
623
+ }
624
+
625
+ /**
626
+ * Per-plugin persistent key-value store backed by the blog database.
627
+ * Available when the plugin declares `capabilities.store`.
628
+ * Keys are namespaced per plugin — no cross-plugin access.
629
+ */
630
+ store: BlogStore
631
+
632
+ /**
633
+ * Read admin-configured plugin settings (set in Plugins → Settings gear icon).
634
+ * Always available. Cached for 30 seconds; changes take effect without restart.
635
+ *
636
+ * @example
637
+ * const apiKey = nuxtblog.settings.get('api_key') as string
638
+ */
639
+ settings: {
640
+ get(key: string): unknown
641
+ }
642
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@nuxtblog/plugin-sdk",
3
+ "version": "0.0.1",
4
+ "description": "TypeScript type definitions and base tsconfig for nuxtblog plugins",
5
+ "types": "index.d.ts",
6
+ "files": [
7
+ "index.d.ts",
8
+ "tsconfig.json"
9
+ ],
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/nuxtblog/plugin-sdk.git"
13
+ },
14
+ "homepage": "https://github.com/nuxtblog/plugin-sdk",
15
+ "bugs": {
16
+ "url": "https://github.com/nuxtblog/plugin-sdk/issues"
17
+ },
18
+ "keywords": [
19
+ "nuxtblog",
20
+ "plugin",
21
+ "sdk",
22
+ "typescript"
23
+ ],
24
+ "license": "MIT",
25
+ "scripts": {
26
+ "release": "npm version patch && npm publish --access public",
27
+ "release:minor": "npm version minor && npm publish --access public",
28
+ "release:major": "npm version major && npm publish --access public"
29
+ }
30
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2015",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2015", "ES2016", "ES2017", "ES2018"],
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "allowSyntheticDefaultImports": true,
10
+ "skipLibCheck": true,
11
+ "noEmit": true,
12
+ "noUnusedLocals": true,
13
+ "noUnusedParameters": true
14
+ },
15
+ "files": ["index.d.ts"]
16
+ }