@voilabs/plugins 0.0.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +634 -0
  2. package/dist/adapters/elysia.d.ts +31 -0
  3. package/dist/adapters/elysia.d.ts.map +1 -0
  4. package/dist/adapters/elysia.js +28 -0
  5. package/dist/adapters/elysia.js.map +1 -0
  6. package/dist/adapters/next.d.ts +25 -0
  7. package/dist/adapters/next.d.ts.map +1 -0
  8. package/dist/adapters/next.js +51 -0
  9. package/dist/adapters/next.js.map +1 -0
  10. package/dist/client.d.ts +33 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +123 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/examples/lumina.d.ts +9 -0
  15. package/dist/examples/lumina.d.ts.map +1 -0
  16. package/dist/examples/lumina.js +197 -0
  17. package/dist/examples/lumina.js.map +1 -0
  18. package/dist/http.d.ts +19 -0
  19. package/dist/http.d.ts.map +1 -0
  20. package/dist/http.js +523 -0
  21. package/dist/http.js.map +1 -0
  22. package/dist/index.d.ts +7 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +7 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/injection.d.ts +10 -0
  27. package/dist/injection.d.ts.map +1 -0
  28. package/dist/injection.js +174 -0
  29. package/dist/injection.js.map +1 -0
  30. package/dist/manager.d.ts +121 -0
  31. package/dist/manager.d.ts.map +1 -0
  32. package/dist/manager.js +1006 -0
  33. package/dist/manager.js.map +1 -0
  34. package/dist/plugin.d.ts +25 -0
  35. package/dist/plugin.d.ts.map +1 -0
  36. package/dist/plugin.js +209 -0
  37. package/dist/plugin.js.map +1 -0
  38. package/dist/react.d.ts +34 -0
  39. package/dist/react.d.ts.map +1 -0
  40. package/dist/react.js +88 -0
  41. package/dist/react.js.map +1 -0
  42. package/dist/types.d.ts +416 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +2 -0
  45. package/dist/types.js.map +1 -0
  46. package/package.json +57 -0
package/README.md ADDED
@@ -0,0 +1,634 @@
1
+ # @voilabs/plugins
2
+
3
+ A framework-agnostic plugin system for Node.js applications. The core package runs on the standard Fetch API, while Next.js, Elysia, and React integrations are provided as optional adapters.
4
+
5
+ ## Features
6
+
7
+ - `Plugin` model with field validation, route manifests, UI component slots, meta tags, permissions, events, webhooks, lifecycle hooks, and injection points.
8
+ - `PluginManager` with local plugin registration, automatic GitHub marketplace sync, install/update/enable/disable/uninstall flows, and tenant-aware storage.
9
+ - HTTP runtime for `/plugins`, `/plugins/:id/install`, `/plugins/:id/config`, and custom plugin routes.
10
+ - WordPress-like injection support: plugins can inject `script`, `style`, `html`, `meta`, `link`, and `noscript` into placements such as `head`, `body:end`, `admin:head`, and `public:footer`.
11
+ - Elysia and Next.js adapters that use structural typing and do not require hard dependencies.
12
+ - React integration through a fetch client and hook factory without making React a peer dependency.
13
+ - GitHub marketplace support through `<plugin-id>.plugin`, `marketplace.json`, `plugins.json`, and `index.json`.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ bun add @voilabs/plugins
19
+ # or
20
+ npm install @voilabs/plugins
21
+ ```
22
+
23
+ The package is ESM-only and targets Node.js 18+ or any modern Fetch-compatible runtime.
24
+
25
+ ## Local Plugin Definition
26
+
27
+ ```ts
28
+ import { Plugin } from "@voilabs/plugins";
29
+
30
+ const LUMINA_API_KEY_ID =
31
+ "^voi_([a-fA-F0-9]{8})(-([a-fA-F0-9]{4})){3}-([a-fA-F0-9]{12})$";
32
+
33
+ export const luminaAIPlugin = new Plugin({
34
+ id: "voidigital-lumina",
35
+ name: "Lumina",
36
+ summary: "Generate articles with Lumina AI.",
37
+ category: "content",
38
+ provider: "voidigital",
39
+ iconUrl: "https://www.google.com/s2/favicons?domain=voidigital.com&sz=128",
40
+ sidebarHref: "/admin/lumina",
41
+ needEncryption: true,
42
+ fields: [
43
+ {
44
+ key: "apiKey",
45
+ label: "API Key",
46
+ type: "secret",
47
+ required: true,
48
+ pattern: LUMINA_API_KEY_ID,
49
+ encrypt: true,
50
+ },
51
+ {
52
+ key: "email",
53
+ label: "Email Address",
54
+ type: "email",
55
+ required: true,
56
+ },
57
+ ],
58
+ routes: [
59
+ {
60
+ method: "POST",
61
+ path: "/articles",
62
+ summary: "Creates an article generation request.",
63
+ handler: async ({ body, config }) => ({
64
+ status: 202,
65
+ body: { queued: true, email: config.email, request: body },
66
+ }),
67
+ },
68
+ ],
69
+ frontend: {
70
+ components: [
71
+ {
72
+ key: "lumina-settings-row",
73
+ slot: "settings-row",
74
+ label: "Lumina settings",
75
+ component: { type: "registry", name: "LuminaSettingsRow" },
76
+ },
77
+ ],
78
+ },
79
+ additionalMetaTags: [
80
+ { key: "lumina-generator", name: "generator", content: "Lumina AI" },
81
+ ],
82
+ injections: [
83
+ {
84
+ key: "lumina-public-style",
85
+ type: "style",
86
+ placement: "head",
87
+ content: ".lumina-badge{font:500 12px system-ui}",
88
+ condition: { area: "public" },
89
+ },
90
+ {
91
+ key: "lumina-widget",
92
+ type: "html",
93
+ placement: "body:end",
94
+ content: '<div class="lumina-badge">Lumina active</div>',
95
+ condition: {
96
+ area: "public",
97
+ excludePaths: ["/admin/*"],
98
+ },
99
+ },
100
+ {
101
+ key: "lumina-admin-script",
102
+ type: "script",
103
+ placement: "admin:footer",
104
+ defer: true,
105
+ content: "window.__LUMINA_PLUGIN__={enabled:true};",
106
+ condition: { area: "admin" },
107
+ },
108
+ ],
109
+ });
110
+ ```
111
+
112
+ When you create a plugin locally with `new Plugin()`, `provider` is required and the exact value you provide is used. For GitHub-loaded `.plugin` files, `provider` and `iconUrl` from the file are ignored.
113
+
114
+ Ready-to-use example: `@voilabs/plugins/examples/lumina`.
115
+
116
+ ## Plugin Manager
117
+
118
+ ```ts
119
+ import { MemoryPluginDatabase, PluginManager } from "@voilabs/plugins";
120
+ import { luminaAIPlugin } from "@voilabs/plugins/examples/lumina";
121
+
122
+ export const plugins = new PluginManager({
123
+ plugins: [luminaAIPlugin],
124
+ marketplaces: ["https://github.com/voilabs/plugins-marketplace"],
125
+ database: new MemoryPluginDatabase(),
126
+ encryption: {
127
+ encrypt: async (value) => value,
128
+ decrypt: async (value) => value,
129
+ },
130
+ });
131
+
132
+ const list = await plugins.get({ page: 1, limit: 10 });
133
+
134
+ await plugins.install("voidigital-lumina", {
135
+ apiKey: "voi_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
136
+ email: "hello@voidigital.com",
137
+ });
138
+ ```
139
+
140
+ When `marketplaces` are provided, GitHub plugins are pulled automatically. The manager prepares itself before the first `get`, `install`, `updateConfig`, `enable`, `disable`, `uninstall`, or HTTP request. To force a refresh:
141
+
142
+ ```ts
143
+ await plugins.syncMarketplaces({ force: true });
144
+ ```
145
+
146
+ Supported GitHub sources:
147
+
148
+ ```ts
149
+ const plugins = new PluginManager({
150
+ marketplaces: [
151
+ "https://github.com/voilabs/plugins-marketplace",
152
+ "voilabs/plugins-marketplace",
153
+ {
154
+ github: "voilabs/private-plugins",
155
+ branch: "main",
156
+ token: process.env.GITHUB_TOKEN,
157
+ files: ["marketplace.json", "plugins.json"],
158
+ pluginFileExtensions: [".plugin"],
159
+ },
160
+ {
161
+ github: "voilabs/plugins-marketplace",
162
+ path: "marketplaces/default",
163
+ },
164
+ ],
165
+ marketplaceRefreshIntervalMs: 5 * 60 * 1000,
166
+ });
167
+ ```
168
+
169
+ By default, a GitHub repository is read from two sources:
170
+
171
+ - `*.plugin` files discovered through the repository tree. File names usually follow `<plugin-id>.plugin`.
172
+ - `marketplace.json`, `plugins.json`, and `index.json` marketplace manifests.
173
+
174
+ If no branch is configured, `main` and `master` are tried. If you provide a GitHub `blob` URL, that exact JSON file is used.
175
+
176
+ ## GitHub `.plugin` Files
177
+
178
+ A `.plugin` file contains one plugin manifest as JSON. For GitHub-loaded plugins:
179
+
180
+ - `provider` is always the repository owner.
181
+ - `iconUrl` is always generated from the repository owner avatar: `https://github.com/<owner>.png?size=128`.
182
+ - Any `provider` or `iconUrl` inside the `.plugin` file is ignored.
183
+
184
+ ```json
185
+ {
186
+ "id": "voidigital-lumina",
187
+ "name": "Lumina",
188
+ "summary": "Generate articles with Lumina AI.",
189
+ "category": "content",
190
+ "fields": [
191
+ {
192
+ "key": "apiKey",
193
+ "label": "API Key",
194
+ "type": "secret",
195
+ "required": true
196
+ }
197
+ ],
198
+ "routes": [
199
+ {
200
+ "id": "create-article",
201
+ "method": "POST",
202
+ "path": "/articles",
203
+ "summary": "Creates an article through Lumina.",
204
+ "proxy": {
205
+ "url": "https://api.lumina.example/v1/articles",
206
+ "method": "POST",
207
+ "headers": {
208
+ "authorization": "Bearer {{config.apiKey}}",
209
+ "content-type": "application/json"
210
+ },
211
+ "body": {
212
+ "title": "{{body.title}}",
213
+ "email": "{{config.email}}"
214
+ }
215
+ }
216
+ }
217
+ ],
218
+ "injections": [
219
+ {
220
+ "key": "lumina-widget",
221
+ "type": "html",
222
+ "placement": "body:end",
223
+ "content": "<div data-lumina-widget></div>"
224
+ }
225
+ ]
226
+ }
227
+ ```
228
+
229
+ Example repository:
230
+
231
+ ```text
232
+ plugins-marketplace/
233
+ voidigital-lumina.plugin
234
+ analytics-pixel.plugin
235
+ commerce/iyzico.plugin
236
+ ```
237
+
238
+ If you set `path`, only `.plugin` files under that folder are read.
239
+
240
+ GitHub-loaded plugins support the same manifest surface as local plugins: routes, frontend components, injections, meta tags, assets, permissions, webhooks, and more. Since JSON cannot carry JavaScript functions, remote plugin routes use declarative actions:
241
+
242
+ ```json
243
+ {
244
+ "routes": [
245
+ {
246
+ "method": "GET",
247
+ "path": "/status",
248
+ "response": {
249
+ "status": 200,
250
+ "body": {
251
+ "ok": true,
252
+ "plugin": "remote"
253
+ }
254
+ }
255
+ },
256
+ {
257
+ "method": "POST",
258
+ "path": "/articles",
259
+ "proxy": {
260
+ "url": "https://api.example.com/articles",
261
+ "method": "POST",
262
+ "headers": {
263
+ "authorization": "Bearer {{config.apiKey}}"
264
+ },
265
+ "forwardBody": true
266
+ }
267
+ },
268
+ {
269
+ "method": "GET",
270
+ "path": "/docs",
271
+ "redirect": {
272
+ "to": "https://docs.example.com/plugins/{{params.id}}",
273
+ "status": 302
274
+ }
275
+ }
276
+ ]
277
+ }
278
+ ```
279
+
280
+ Route template variables:
281
+
282
+ - `{{params.id}}`
283
+ - `{{query.page}}`
284
+ - `{{body.title}}`
285
+ - `{{config.apiKey}}`
286
+ - `{{tenantId}}`
287
+ - `{{locals.userId}}`
288
+
289
+ Declarative routes run only when the plugin is installed and enabled by default. Override this per route with `requiresInstallation` and `requiresEnabled`.
290
+
291
+ ## Custom Database Adapter
292
+
293
+ ```ts
294
+ const plugins = new PluginManager({
295
+ database: {
296
+ load: async (pluginId, scope) => null,
297
+ save: async (installation) => installation,
298
+ update: async (pluginId, patch, scope) => undefined,
299
+ delete: async (pluginId, scope) => undefined,
300
+ },
301
+ });
302
+ ```
303
+
304
+ ## Elysia Adapter
305
+
306
+ ```ts
307
+ import { Elysia } from "elysia";
308
+ import { elysiaPluginRoutes } from "@voilabs/plugins/adapters/elysia";
309
+ import { plugins } from "./plugins";
310
+
311
+ new Elysia()
312
+ .use(elysiaPluginRoutes(plugins, { prefix: "/plugins" }))
313
+ .listen(3000);
314
+ ```
315
+
316
+ Example endpoints:
317
+
318
+ - `GET /plugins?page=1&limit=10`
319
+ - `GET /plugins/voidigital-lumina`
320
+ - `POST /plugins/voidigital-lumina/install`
321
+ - `PATCH /plugins/voidigital-lumina/config`
322
+ - `POST /plugins/voidigital-lumina/articles`
323
+ - `GET /plugins/injections?area=public&placement=head`
324
+
325
+ ## Script and HTML Injection
326
+
327
+ Plugins can define static or dynamic injections:
328
+
329
+ ```ts
330
+ const analyticsPlugin = new Plugin({
331
+ id: "analytics-pixel",
332
+ name: "Analytics Pixel",
333
+ summary: "Adds a tracking script to the site.",
334
+ category: "analytics",
335
+ provider: "voilabs",
336
+ fields: [
337
+ { key: "pixelId", label: "Pixel ID", type: "text", required: true },
338
+ ],
339
+ injections: [
340
+ {
341
+ key: "pixel-script",
342
+ type: "script",
343
+ placement: "public:footer",
344
+ async: true,
345
+ render: ({ config }) => `
346
+ window.pixelId=${JSON.stringify(config.pixelId)};
347
+ console.log("pixel loaded");
348
+ `,
349
+ },
350
+ {
351
+ key: "verify-meta",
352
+ type: "meta",
353
+ placement: "head",
354
+ attributes: {
355
+ name: "analytics-verification",
356
+ content: "abc123",
357
+ },
358
+ },
359
+ ],
360
+ });
361
+ ```
362
+
363
+ Inject into a full SSR HTML document:
364
+
365
+ ```ts
366
+ const html = await renderPage();
367
+
368
+ return await plugins.injectHtml(html, {
369
+ area: "public",
370
+ url: "https://example.com/blog/hello",
371
+ nonce: cspNonce,
372
+ });
373
+ ```
374
+
375
+ Render only a specific placement:
376
+
377
+ ```ts
378
+ const headHtml = await plugins.renderInjections({
379
+ area: "public",
380
+ placement: "head",
381
+ nonce: cspNonce,
382
+ });
383
+ ```
384
+
385
+ Only installed and enabled plugins can inject by default. To include marketplace plugins that are not installed:
386
+
387
+ ```ts
388
+ await plugins.renderInjections({
389
+ area: "public",
390
+ installedOnly: false,
391
+ enabledOnly: false,
392
+ });
393
+ ```
394
+
395
+ ## Next.js App Router Adapter
396
+
397
+ ```ts
398
+ // app/api/plugins/[...voilabs]/route.ts
399
+ import { createNextPluginRouteHandlers } from "@voilabs/plugins/adapters/next";
400
+ import { plugins } from "@/server/plugins";
401
+
402
+ export const { GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD } =
403
+ createNextPluginRouteHandlers(plugins, {
404
+ prefix: "/api/plugins",
405
+ });
406
+ ```
407
+
408
+ ## React Integration
409
+
410
+ React is not imported by the package. Pass your app's React object into the integration factory.
411
+
412
+ ```tsx
413
+ import * as React from "react";
414
+ import { createPluginReactIntegration } from "@voilabs/plugins/react";
415
+
416
+ const { PluginProvider, usePlugins } = createPluginReactIntegration(React, {
417
+ baseUrl: "/api/plugins",
418
+ });
419
+
420
+ function PluginList() {
421
+ const { data, loading, error } = usePlugins({ page: 1, limit: 10 });
422
+
423
+ if (loading) return <div>Loading</div>;
424
+ if (error) return <div>Something went wrong</div>;
425
+
426
+ return (
427
+ <ul>
428
+ {data?.data.map((plugin) => (
429
+ <li key={plugin.id}>{plugin.name}</li>
430
+ ))}
431
+ </ul>
432
+ );
433
+ }
434
+
435
+ export function App() {
436
+ return (
437
+ <PluginProvider>
438
+ <PluginList />
439
+ </PluginProvider>
440
+ );
441
+ }
442
+ ```
443
+
444
+ ## Marketplace JSON Format
445
+
446
+ Marketplace JSON can be either an array or an object:
447
+
448
+ ```json
449
+ {
450
+ "name": "Voilabs Plugins",
451
+ "plugins": [
452
+ {
453
+ "id": "voidigital-lumina",
454
+ "name": "Lumina",
455
+ "summary": "Generate articles with Lumina AI.",
456
+ "category": "content",
457
+ "fields": []
458
+ }
459
+ ]
460
+ }
461
+ ```
462
+
463
+ When a GitHub repository URL is provided, `<plugin-id>.plugin` files are discovered first. `marketplace.json`, `plugins.json`, and `index.json` are also tried.
464
+
465
+ ## Integration Prompts
466
+
467
+ Use one of the following prompts inside the target project when you want a coding agent to integrate `@voilabs/plugins`.
468
+
469
+ ### Prompt: Next.js + Elysia.js
470
+
471
+ ```text
472
+ Integrate @voilabs/plugins into this project using Next.js for the frontend/admin UI and Elysia.js for the plugin API runtime.
473
+
474
+ Goals:
475
+ - Use GitHub marketplace repositories as the plugin source.
476
+ - Automatically discover <plugin-id>.plugin files from the configured GitHub repo.
477
+ - Treat the GitHub repo owner as the provider for remote plugins.
478
+ - Use the GitHub owner avatar as the plugin icon.
479
+ - Ignore provider and iconUrl values inside remote .plugin files.
480
+ - Support plugin fields, install/update/enable/disable/uninstall, declarative routes, proxy routes, redirects, frontend component manifests, and HTML/script/style injections.
481
+
482
+ Backend/Elysia work:
483
+ 1. Create a singleton plugin manager, for example in src/server/plugins.ts:
484
+ - import { PluginManager, MemoryPluginDatabase } from "@voilabs/plugins"
485
+ - configure marketplaces: ["<github-owner>/<plugins-repo>"]
486
+ - use MemoryPluginDatabase initially if no persistent storage exists
487
+ - add an encryption provider placeholder, but keep the API ready for production encryption
488
+ 2. Mount the plugin API into Elysia:
489
+ - import { elysiaPluginRoutes } from "@voilabs/plugins/adapters/elysia"
490
+ - use elysiaPluginRoutes(plugins, { prefix: "/plugins" })
491
+ 3. Make sure the Elysia server exposes:
492
+ - GET /plugins
493
+ - GET /plugins/:pluginId
494
+ - POST /plugins/:pluginId/install
495
+ - PATCH /plugins/:pluginId/config
496
+ - POST /plugins/:pluginId/enable
497
+ - POST /plugins/:pluginId/disable
498
+ - DELETE /plugins/:pluginId
499
+ - custom plugin routes such as /plugins/:pluginId/articles
500
+ - GET /plugins/injections?area=public&placement=head
501
+ 4. If Next.js and Elysia run on different ports, proxy /api/plugins/* from Next.js to the Elysia /plugins/* API.
502
+
503
+ Next.js/React work:
504
+ 1. Add createPluginReactIntegration from @voilabs/plugins/react.
505
+ 2. Create a PluginProvider bound to the Elysia API base URL, usually /api/plugins.
506
+ 3. Build or wire an admin plugin screen:
507
+ - list plugins with iconUrl, name, provider, summary, category, installed/enabled state
508
+ - generate config forms from plugin.fields
509
+ - support secret fields without echoing plain secret values back into the UI
510
+ - install, update config, enable, disable, and uninstall plugins
511
+ 4. Render frontend component manifests through a local component registry:
512
+ - support component.type === "registry"
513
+ - ignore unknown registry components safely
514
+ 5. Add injection support:
515
+ - head injections: render via the API or server helper and place into the document head
516
+ - body/footer injections: provide slots in the app layout
517
+ - pass CSP nonce if this project uses CSP
518
+
519
+ Verification:
520
+ - Run TypeScript checks and the project build.
521
+ - Verify GitHub .plugin discovery with at least one plugin that does not define provider or iconUrl.
522
+ - Confirm the UI shows provider as the GitHub owner and icon as the GitHub avatar.
523
+ - Confirm install + enable works.
524
+ - Confirm a declarative route using response/proxy/redirect works.
525
+ - Confirm script/style/html injections render only for installed and enabled plugins by default.
526
+ ```
527
+
528
+ ### Prompt: Native Next.js
529
+
530
+ ```text
531
+ Integrate @voilabs/plugins into this native Next.js project.
532
+
533
+ Goals:
534
+ - Use Next.js App Router API routes for the plugin runtime.
535
+ - Use React integration for the admin/plugin UI.
536
+ - Load remote plugins from GitHub marketplace repositories and <plugin-id>.plugin files.
537
+ - Remote plugin provider must always be the GitHub repo owner.
538
+ - Remote plugin iconUrl must always be the GitHub owner avatar.
539
+ - Ignore provider and iconUrl fields inside GitHub-loaded plugin files.
540
+
541
+ Server work:
542
+ 1. Create src/server/plugins.ts:
543
+ - instantiate PluginManager from @voilabs/plugins
544
+ - configure marketplaces: ["<github-owner>/<plugins-repo>"]
545
+ - use MemoryPluginDatabase as a temporary adapter if no DB adapter exists
546
+ - add encryption provider hooks for secret fields
547
+ 2. Create app/api/plugins/[...voilabs]/route.ts:
548
+ - import createNextPluginRouteHandlers from @voilabs/plugins/adapters/next
549
+ - export GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD from createNextPluginRouteHandlers(plugins, { prefix: "/api/plugins" })
550
+ 3. Ensure the following routes work:
551
+ - GET /api/plugins
552
+ - GET /api/plugins/:pluginId
553
+ - POST /api/plugins/:pluginId/install
554
+ - PATCH /api/plugins/:pluginId/config
555
+ - POST /api/plugins/:pluginId/enable
556
+ - POST /api/plugins/:pluginId/disable
557
+ - custom plugin routes under /api/plugins/:pluginId/*
558
+
559
+ React/admin work:
560
+ 1. Create a plugin React integration module:
561
+ - import * as React from "react"
562
+ - import { createPluginReactIntegration } from "@voilabs/plugins/react"
563
+ - set baseUrl to "/api/plugins"
564
+ 2. Wrap the admin app or layout with PluginProvider.
565
+ 3. Build or integrate an admin Plugins page:
566
+ - use usePlugins({ page: 1, limit: 20 })
567
+ - show icon, name, provider, summary, category, installed/enabled status
568
+ - generate install/config forms from plugin.fields
569
+ - handle secret fields carefully and do not display stored secrets as plain text
570
+ - wire install, updateConfig, enable, disable, uninstall
571
+ 4. Add support for plugin frontend manifests:
572
+ - resolve frontend.components with a local registry map
573
+ - render known registry components in their slots
574
+ 5. Add injection support:
575
+ - for server layouts, call plugins.renderInjections({ area: "public", placement: "head", nonce })
576
+ - create safe body/footer slots for public and admin injections
577
+ - ensure injections are rendered only for installed and enabled plugins by default
578
+
579
+ Verification:
580
+ - Run lint/typecheck/build.
581
+ - Test a GitHub .plugin file without provider/iconUrl.
582
+ - Confirm provider is GitHub owner and iconUrl is https://github.com/<owner>.png?size=128.
583
+ - Confirm declarative response, proxy, and redirect routes work.
584
+ - Confirm plugin injections render in the expected placements.
585
+ ```
586
+
587
+ ### Prompt: Native Elysia.js
588
+
589
+ ```text
590
+ Integrate @voilabs/plugins into this native Elysia.js project.
591
+
592
+ Goals:
593
+ - Use Elysia as the plugin API runtime.
594
+ - Load plugins from GitHub marketplace repositories and <plugin-id>.plugin files.
595
+ - GitHub-loaded plugin provider must be the GitHub repo owner.
596
+ - GitHub-loaded plugin iconUrl must be the GitHub owner avatar.
597
+ - Ignore provider and iconUrl fields inside remote .plugin files.
598
+ - Support install/config/enable/disable/uninstall, declarative routes, proxy routes, redirects, and injection rendering endpoints.
599
+
600
+ Implementation:
601
+ 1. Create a plugin manager singleton:
602
+ - import { PluginManager, MemoryPluginDatabase } from "@voilabs/plugins"
603
+ - configure marketplaces: ["<github-owner>/<plugins-repo>"]
604
+ - use MemoryPluginDatabase if no persistent DB exists yet
605
+ - add an encryption provider placeholder for secret fields
606
+ 2. Mount routes in Elysia:
607
+ - import { elysiaPluginRoutes } from "@voilabs/plugins/adapters/elysia"
608
+ - new Elysia().use(elysiaPluginRoutes(plugins, { prefix: "/plugins" }))
609
+ 3. Verify standard endpoints:
610
+ - GET /plugins
611
+ - GET /plugins/:pluginId
612
+ - POST /plugins/:pluginId/install
613
+ - PATCH /plugins/:pluginId/config
614
+ - POST /plugins/:pluginId/enable
615
+ - POST /plugins/:pluginId/disable
616
+ - DELETE /plugins/:pluginId
617
+ - GET /plugins/injections?area=public&placement=head
618
+ 4. Support custom remote plugin routes:
619
+ - response
620
+ - proxy
621
+ - redirect
622
+ - template variables like {{config.apiKey}}, {{body.title}}, {{query.page}}, {{params.id}}
623
+ 5. If this project serves HTML from Elysia:
624
+ - call await plugins.injectHtml(html, { area: "public", url: request.url, nonce })
625
+ - use plugins.renderInjections for specific head/body/footer slots when full HTML post-processing is not appropriate
626
+
627
+ Verification:
628
+ - Run typecheck/build/tests.
629
+ - Mock or use a real GitHub marketplace with at least one <plugin-id>.plugin file.
630
+ - Confirm provider and iconUrl are derived from the GitHub repo owner.
631
+ - Install and enable the plugin.
632
+ - Confirm declarative response/proxy/redirect routes work.
633
+ - Confirm injections are available through /plugins/injections and render only for installed/enabled plugins by default.
634
+ ```
@@ -0,0 +1,31 @@
1
+ import type { PluginHttpHandlerOptions } from "../http.js";
2
+ import type { PluginManager } from "../manager.js";
3
+ export interface ElysiaLike {
4
+ get?: (path: string, handler: (context: {
5
+ request: Request;
6
+ }) => unknown) => this;
7
+ post?: (path: string, handler: (context: {
8
+ request: Request;
9
+ }) => unknown) => this;
10
+ put?: (path: string, handler: (context: {
11
+ request: Request;
12
+ }) => unknown) => this;
13
+ patch?: (path: string, handler: (context: {
14
+ request: Request;
15
+ }) => unknown) => this;
16
+ delete?: (path: string, handler: (context: {
17
+ request: Request;
18
+ }) => unknown) => this;
19
+ options?: (path: string, handler: (context: {
20
+ request: Request;
21
+ }) => unknown) => this;
22
+ head?: (path: string, handler: (context: {
23
+ request: Request;
24
+ }) => unknown) => this;
25
+ all?: (path: string, handler: (context: {
26
+ request: Request;
27
+ }) => unknown) => this;
28
+ }
29
+ export declare function mountElysiaPluginRoutes<TApp extends ElysiaLike>(app: TApp, manager: PluginManager, options?: PluginHttpHandlerOptions): TApp;
30
+ export declare function elysiaPluginRoutes(manager: PluginManager, options?: PluginHttpHandlerOptions): <TApp extends ElysiaLike>(app: TApp) => TApp;
31
+ //# sourceMappingURL=elysia.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elysia.d.ts","sourceRoot":"","sources":["../../src/adapters/elysia.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,KAAK,IAAI,CAAC;IAClF,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,KAAK,IAAI,CAAC;IACnF,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,KAAK,IAAI,CAAC;IAClF,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,KAAK,IAAI,CAAC;IACpF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,KAAK,IAAI,CAAC;IACrF,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,KAAK,IAAI,CAAC;IACtF,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,KAAK,IAAI,CAAC;IACnF,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,KAAK,IAAI,CAAC;CACnF;AAED,wBAAgB,uBAAuB,CAAC,IAAI,SAAS,UAAU,EAC7D,GAAG,EAAE,IAAI,EACT,OAAO,EAAE,aAAa,EACtB,OAAO,GAAE,wBAA6B,GACrC,IAAI,CAqBN;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,aAAa,EACtB,OAAO,GAAE,wBAA6B,IAE9B,IAAI,SAAS,UAAU,EAAE,KAAK,IAAI,KAAG,IAAI,CAElD"}
@@ -0,0 +1,28 @@
1
+ import { createPluginHttpHandler } from "../http.js";
2
+ export function mountElysiaPluginRoutes(app, manager, options = {}) {
3
+ const prefix = normalizePrefix(options.prefix ?? "/plugins");
4
+ const handler = createPluginHttpHandler(manager, {
5
+ ...options,
6
+ prefix,
7
+ });
8
+ const handle = ({ request }) => handler(request);
9
+ if (app.all) {
10
+ app.all(prefix, handle);
11
+ app.all(`${prefix}/*`, handle);
12
+ return app;
13
+ }
14
+ const methods = ["get", "post", "put", "patch", "delete", "options", "head"];
15
+ for (const method of methods) {
16
+ app[method]?.(prefix, handle);
17
+ app[method]?.(`${prefix}/*`, handle);
18
+ }
19
+ return app;
20
+ }
21
+ export function elysiaPluginRoutes(manager, options = {}) {
22
+ return (app) => mountElysiaPluginRoutes(app, manager, options);
23
+ }
24
+ function normalizePrefix(prefix) {
25
+ const normalized = prefix.startsWith("/") ? prefix : `/${prefix}`;
26
+ return normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
27
+ }
28
+ //# sourceMappingURL=elysia.js.map