diff --git a/apps/web/content/docs/content-engine/admincp.mdx b/apps/web/content/docs/content-engine/admincp.mdx index 9a581ecb1f..55df952a10 100644 --- a/apps/web/content/docs/content-engine/admincp.mdx +++ b/apps/web/content/docs/content-engine/admincp.mdx @@ -172,14 +172,14 @@ import type { ContentFrontendPluginSource } from "@vitnode/core/lib/plugin"; import { contentTypeAdmin } from "@vitnode/core/lib/plugin"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { BlogCategoryColorCell } from "@/views/admin/category/color-cell"; import { BlogCategoryColorField } from "@/views/admin/category/color-field"; import { blogCategoryNav } from "./nav"; export const adminContent = { - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, contentTypes: [ contentTypeAdmin({ ...blogCategoryNav, diff --git a/apps/web/content/docs/content-engine/container-frontend.mdx b/apps/web/content/docs/content-engine/container-frontend.mdx index 7fce7f05a5..94078aae60 100644 --- a/apps/web/content/docs/content-engine/container-frontend.mdx +++ b/apps/web/content/docs/content-engine/container-frontend.mdx @@ -76,11 +76,11 @@ Create a helper that reads every board. The list comes back in tree order, flat, ```ts title="plugins/example/src/pages/boards-data.ts" import { fetcher } from "@vitnode/core/tanstack/fetcher"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; export const fetchBoards = async () => { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/boards", path: "/", @@ -255,7 +255,7 @@ import { contentPageItem } from "@vitnode/core/tanstack/content"; import { fetcher } from "@vitnode/core/tanstack/fetcher"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import type { BoardSearch } from "./board-search"; @@ -268,7 +268,7 @@ const zodDiscussionRow = z.object({ const loadBoard = async (idOrSlug: string, { cursor }: BoardSearch) => { const detail = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/boards", path: "/{idOrSlug}", @@ -290,7 +290,7 @@ const loadBoard = async (idOrSlug: string, { cursor }: BoardSearch) => { }; const list = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/discussions", path: "/", @@ -431,7 +431,7 @@ const onSubmit: AutoFormOnSubmit = async ( form, ) => { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "content/discussions", path: "/", diff --git a/apps/web/content/docs/content-engine/container-hierarchy.mdx b/apps/web/content/docs/content-engine/container-hierarchy.mdx index 74af6928b0..96a42ce2bf 100644 --- a/apps/web/content/docs/content-engine/container-hierarchy.mdx +++ b/apps/web/content/docs/content-engine/container-hierarchy.mdx @@ -69,16 +69,15 @@ A collection holds at most 5,000 containers (`limitReached`, 409). The whole tre `createContentContainerService` is the one way to read and change a collection. The AdminCP routes and the public routes both use it, so your code gets the same rules, cache expiry and events. Call it inside any Hono handler with the request context: ```ts title="plugins/example/src/api/modules/boards/archive.route.ts" -import { buildRoute } from "@vitnode/core/api/lib/route"; import { createContentContainerService } from "@vitnode/core/content/server"; import { HTTPException } from "hono/http-exception"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { buildRoute } from "@/api/builders"; import { boardContainers } from "@/content/boards"; +import { manifest } from "@/manifest.gen"; export const archiveBoardRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "example_boards", permission: "can_edit" }, route: { method: "post", @@ -94,7 +93,7 @@ export const archiveBoardRoute = buildRoute({ const { id } = c.req.valid("param"); const boards = createContentContainerService(c, { definition: boardContainers, - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, }); const archive = await boards.findBySlug("archive"); diff --git a/apps/web/content/docs/content-engine/containers.mdx b/apps/web/content/docs/content-engine/containers.mdx index 20fdfb1b04..917628df53 100644 --- a/apps/web/content/docs/content-engine/containers.mdx +++ b/apps/web/content/docs/content-engine/containers.mdx @@ -130,7 +130,6 @@ Pass the definition to both content modules. The admin module adds the staff rou import { boardContainers } from "@/content/boards"; // [!code ++] buildContentAdminModule({ - pluginId: CONFIG_PLUGIN.pluginId, containers: [boardContainers], // [!code ++] contentTypes: [articleContent, discussionContent], }), @@ -138,7 +137,6 @@ buildContentAdminModule({ ```ts title="plugins/example/src/config.api.ts" buildContentPublicModule({ - pluginId: CONFIG_PLUGIN.pluginId, containers: [boardContainers], // [!code ++] contentTypes: [articleContent, discussionContent], }), @@ -166,7 +164,7 @@ export const exampleBoardsNav = { }; export const adminNav = { - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, containers: [exampleBoardsNav], // [!code ++] contentTypes: [exampleArticleNav, exampleDiscussionNav], } satisfies AdminNavPluginSource; @@ -174,7 +172,7 @@ export const adminNav = { ```tsx title="plugins/example/src/admin/content.tsx" export const adminContent = { - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, containers: [exampleBoardsNav], // [!code ++] contentTypes: [exampleArticleNav, exampleDiscussionNav], } satisfies ContentFrontendPluginSource; diff --git a/apps/web/content/docs/content-engine/create-a-content-type.mdx b/apps/web/content/docs/content-engine/create-a-content-type.mdx index 8e4616411b..e7c35d79aa 100644 --- a/apps/web/content/docs/content-engine/create-a-content-type.mdx +++ b/apps/web/content/docs/content-engine/create-a-content-type.mdx @@ -16,13 +16,20 @@ Every snippet is copied from `plugins/example`, with unrelated content types tri ## Before you begin -You need a plugin that your app already loads, with `src/config.api.ts`, `src/config.tsx` and a `src/const.ts` that exports the plugin id: +You need a plugin that your app already loads, with `src/config.api.ts`, `src/config.tsx` and `src/api/builders.ts`, which binds the API and Content Engine builders to your plugin, so none of them needs a plugin id: -```ts title="plugins/example/src/const.ts" -export const CONFIG_PLUGIN = { pluginId: "@vitnode/example" as const }; +```ts title="plugins/example/src/api/builders.ts" +import { bindApiPlugin } from "@vitnode/core/api/lib/bind-api-plugin"; +import { bindContentPlugin } from "@vitnode/core/content/server"; + +import { manifest } from "@/manifest.gen"; + +export const { buildModule, buildRoute } = bindApiPlugin(manifest); +export const { buildContentAdminModule, buildContentPublicModule } = + bindContentPlugin(manifest); ``` -[Create a plugin](/docs/dev/plugins/create) generates all three. You also need a running PostgreSQL database and an AdminCP account. +`src/manifest.gen.ts` is written by `vitnode build` and `vitnode dev` from your `package.json`, and `manifest.id` is the plugin id. [Create a plugin](/docs/dev/plugins/create) generates all of these. You also need a running PostgreSQL database and an AdminCP account. @@ -87,19 +94,14 @@ Export the table. Drizzle Kit finds tables by reading the exports of your plugin Create an `admin` module and pass the model to `buildContentAdminModule`: ```ts title="plugins/example/src/api/modules/admin/admin.module.ts" -import { buildModule } from "@vitnode/core/api/lib/module"; -import { buildContentAdminModule } from "@vitnode/core/content/server"; - -import { CONFIG_PLUGIN } from "@/const"; +import { buildContentAdminModule, buildModule } from "@/api/builders"; import { categoryContent } from "@/database/categories"; export const adminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "admin", routes: [], modules: [ buildContentAdminModule({ - pluginId: CONFIG_PLUGIN.pluginId, contentTypes: [categoryContent], }), ], @@ -113,7 +115,7 @@ import { adminModule } from "@/api/modules/admin/admin.module"; // [!code ++] export const exampleApiPlugin = () => buildApiPlugin({ - pluginId: CONFIG_PLUGIN.pluginId, + manifest, modules: [adminModule], // [!code highlight] }); ``` @@ -133,11 +135,11 @@ Create `src/admin/nav.tsx` with the sidebar link: ```tsx title="plugins/example/src/admin/nav.tsx" import { FolderIcon } from "lucide-react"; -import { CONFIG_PLUGIN } from "@/const"; import { categoryContentType } from "@/content/category"; +import { manifest } from "@/manifest.gen"; export const adminNav = { - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, contentTypes: [{ definition: categoryContentType, icon: }], }; ``` diff --git a/apps/web/content/docs/content-engine/duplicate-content.mdx b/apps/web/content/docs/content-engine/duplicate-content.mdx index 18d7e3a856..ae127369f9 100644 --- a/apps/web/content/docs/content-engine/duplicate-content.mdx +++ b/apps/web/content/docs/content-engine/duplicate-content.mdx @@ -129,7 +129,7 @@ import { contentDuplicateEffects, resolveContentActor } from "@vitnode/core/cont const { editorialService } = postContent; if (!editorialService) throw new Error("Articles have no editorial features."); -const pluginId = CONFIG_PLUGIN.pluginId; +const pluginId = manifest.id; const actor = resolveContentActor(c); const outcome = await editorialService(c, { pluginId }).duplicate(sourceId, { actor, diff --git a/apps/web/content/docs/content-engine/events.mdx b/apps/web/content/docs/content-engine/events.mdx index 7d9402bee5..cdbe9637bc 100644 --- a/apps/web/content/docs/content-engine/events.mdx +++ b/apps/web/content/docs/content-engine/events.mdx @@ -108,12 +108,10 @@ VitNode collects listeners only from the `events` array of a module passed direc ```ts title="plugins/example/src/api/modules/admin/admin.module.ts" export const adminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "admin", routes: [], modules: [ buildContentAdminModule({ - pluginId: CONFIG_PLUGIN.pluginId, contentTypes: [articleContent], }), ], diff --git a/apps/web/content/docs/content-engine/localization.mdx b/apps/web/content/docs/content-engine/localization.mdx index 2b8bbdf45f..1ff915ea4a 100644 --- a/apps/web/content/docs/content-engine/localization.mdx +++ b/apps/web/content/docs/content-engine/localization.mdx @@ -150,7 +150,7 @@ In a page loader, pass the route's locale through. This is the example plugin's ```ts title="plugins/example/src/pages/localized-article-page.tsx" const detail = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/localized-articles", path: "/{slug}", diff --git a/apps/web/content/docs/content-engine/plugin-files.mdx b/apps/web/content/docs/content-engine/plugin-files.mdx index 3bff30e051..7e2442a503 100644 --- a/apps/web/content/docs/content-engine/plugin-files.mdx +++ b/apps/web/content/docs/content-engine/plugin-files.mdx @@ -18,7 +18,9 @@ VitNode reads a plugin's content types from several small files instead of one b | `src/admin/content.tsx` | `adminContent` | The content screens, loaded on demand | Screen registrations, custom cells, fields and form layouts | | `src/content.ts` | `contentTypes` | The web build and the API at startup | Every content type with `delivery` or `search` | | `src/routes.ts` | `routes` | The web build | The pages that serve `delivery` and `search` URLs | -| `src/config.tsx` | The plugin | The app's `vitnode.config.ts` | Plugin id, `messages`, `localeFiles` and `routes` only | +| `src/config.tsx` | The plugin | The app's `vitnode.config.ts` | `manifest`, `messages`, `localeFiles` and `routes` only | +| `src/manifest.gen.ts` | `manifest` | Every file that needs the plugin id | The plugin id as `manifest.id`. Written by `vitnode build` / `dev` | +| `src/api/builders.ts` | `buildModule`, `buildRoute`, `buildContent*Module` | Every API module and route file | The builders bound to `manifest` by `bindApiPlugin` and `bindContentPlugin` | | `src/locales/en.json` | JSON | The app, through `localeFiles` | Labels for content types, fields and permissions | A content type with an AdminCP screen needs an entry in both `admin/nav` and `admin/content`. The first adds the sidebar link, the second the screen behind it. They are separate files so the sidebar stays small on every AdminCP page, while form and editor code loads only with the screen. @@ -34,12 +36,12 @@ Once you customize a screen, build `adminContent` on its own and reuse the nav e ```tsx title="plugins/example/src/admin/content.tsx" import type { ContentFrontendPluginSource } from "@vitnode/core/lib/plugin"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { exampleArticleNav, exampleCategoryNav, examplePageNav } from "./nav"; export const adminContent = { - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, contentTypes: [exampleArticleNav, exampleCategoryNav, examplePageNav], } satisfies ContentFrontendPluginSource; ``` @@ -83,14 +85,13 @@ Tables are the exception. Drizzle Kit reads `node_modules//dist/src/da ```tsx title="plugins/example/src/config.tsx" import { buildPlugin } from "@vitnode/core/lib/plugin"; -import { CONFIG_PLUGIN } from "@/const"; - import messages from "./locales"; +import { manifest } from "./manifest.gen"; import { routes } from "./routes"; export const examplePlugin = () => buildPlugin({ - ...CONFIG_PLUGIN, + manifest, localeFiles: { en: "@vitnode/example/locales/en.json", }, diff --git a/apps/web/content/docs/content-engine/public-api.mdx b/apps/web/content/docs/content-engine/public-api.mdx index 1a834fe7fb..ff50306dbd 100644 --- a/apps/web/content/docs/content-engine/public-api.mdx +++ b/apps/web/content/docs/content-engine/public-api.mdx @@ -67,18 +67,17 @@ export const articleContentType = defineContentType({ Add `buildContentPublicModule` to the plugin's API config. The example plugin passes all five content models; one without `publicApi` is skipped: ```ts title="plugins/example/src/config.api.ts" -import { buildContentPublicModule } from "@vitnode/core/content/server"; // [!code ++] +import { buildContentPublicModule } from "@/api/builders"; // [!code ++] import { articleContent } from "@/database/articles"; // [!code ++] export const exampleApiPlugin = () => buildApiPlugin({ - pluginId: CONFIG_PLUGIN.pluginId, + manifest, modules: [ adminModule, - // [!code ++:4] + // [!code ++:3] buildContentPublicModule({ - pluginId: CONFIG_PLUGIN.pluginId, contentTypes: [articleContent], }), ], diff --git a/apps/web/content/docs/content-engine/public-pages.mdx b/apps/web/content/docs/content-engine/public-pages.mdx index 4ab984be1c..8dc4f7ff56 100644 --- a/apps/web/content/docs/content-engine/public-pages.mdx +++ b/apps/web/content/docs/content-engine/public-pages.mdx @@ -105,7 +105,7 @@ import { import { fetcher } from "@vitnode/core/tanstack/fetcher"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { ContentArticle } from "./content-article"; @@ -118,14 +118,14 @@ const zodArticle = z.object({ const loadArticle = async (slug: string) => { const [resolution, detail] = await Promise.all([ fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/articles", path: "/delivery/resolve/{slug}", args: { params: { slug: encodeURIComponent(slug) } }, }), fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/articles", path: "/{slug}", diff --git a/apps/web/content/docs/content-engine/public-write-api.mdx b/apps/web/content/docs/content-engine/public-write-api.mdx index a8feee0e83..15ef0b7778 100644 --- a/apps/web/content/docs/content-engine/public-write-api.mdx +++ b/apps/web/content/docs/content-engine/public-write-api.mdx @@ -330,7 +330,6 @@ The methods throw instead of answering: A route that starts a discussion from a single question. `withHttpErrors` turns a unique conflict into the structured `409`, and the `catch` maps a refusal to its status: ```ts title="plugins/example/src/api/modules/discussions/ask.route.ts" -import { buildRoute } from "@vitnode/core/api/lib/route"; import { ContentPublicWriteDenied } from "@vitnode/core/content"; import { createContentPublicWriteService, @@ -339,11 +338,11 @@ import { import { HTTPException } from "hono/http-exception"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { buildRoute } from "@/api/builders"; import { discussionContent } from "@/database/discussions"; +import { manifest } from "@/manifest.gen"; export const askRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", path: "/ask", @@ -377,7 +376,7 @@ export const askRoute = buildRoute({ const service = createContentPublicWriteService({ c, model: discussionContent, - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, }); try { diff --git a/apps/web/content/docs/content-engine/public-write-frontend.mdx b/apps/web/content/docs/content-engine/public-write-frontend.mdx index f68498dd8f..dd5a12789f 100644 --- a/apps/web/content/docs/content-engine/public-write-frontend.mdx +++ b/apps/web/content/docs/content-engine/public-write-frontend.mdx @@ -305,7 +305,7 @@ import { fetcher } from "@vitnode/core/tanstack/fetcher"; import { useState } from "react"; import { useTranslations } from "use-intl"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { DiscussionForm, @@ -339,7 +339,7 @@ const DiscussionNewPage = () => { form, ) => { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "content/discussions", path: "/", @@ -422,7 +422,7 @@ import { useState } from "react"; import { toast } from "sonner"; import { useTranslations } from "use-intl"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { DiscussionForm, @@ -437,7 +437,7 @@ const loadEditableDiscussion = async (param: string) => { } const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/discussions", path: "/{id}/edit", @@ -490,7 +490,7 @@ const DiscussionEditForm = ({ record }: { record: EditableDiscussion }) => { form, ) => { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "patch", module: "content/discussions", path: "/{id}", @@ -657,7 +657,7 @@ const DiscussionNewPage = () => { { captchaToken }, // [!code ++] ) => { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "content/discussions", path: "/", @@ -691,7 +691,7 @@ import { fetcher } from "@vitnode/core/tanstack/fetcher"; import { useState } from "react"; import { useTranslations } from "use-intl"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; const toDocument = (text: string): RichTextDocument => ({ type: "doc", @@ -720,7 +720,7 @@ export const QuickDiscussionForm = () => { setIsPending(true); const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "content/discussions", path: "/", diff --git a/apps/web/content/docs/content-engine/public-write.mdx b/apps/web/content/docs/content-engine/public-write.mdx index ea90b3fe5e..cbf3c9f34d 100644 --- a/apps/web/content/docs/content-engine/public-write.mdx +++ b/apps/web/content/docs/content-engine/public-write.mdx @@ -109,11 +109,10 @@ import { discussionContent } from "@/database/discussions"; // [!code ++] export const exampleApiPlugin = () => buildApiPlugin({ - pluginId: CONFIG_PLUGIN.pluginId, + manifest, modules: [ adminModule, buildContentPublicModule({ - pluginId: CONFIG_PLUGIN.pluginId, contentTypes: [articleContent, discussionContent], // [!code highlight] }), ], diff --git a/apps/web/content/docs/content-engine/services.mdx b/apps/web/content/docs/content-engine/services.mdx index 208bf74277..f172b2f9e6 100644 --- a/apps/web/content/docs/content-engine/services.mdx +++ b/apps/web/content/docs/content-engine/services.mdx @@ -50,14 +50,12 @@ The three sections below build one real example for each. ### Write the route ```ts title="plugins/example/src/api/modules/articles/by-category.route.ts" -import { buildRoute } from "@vitnode/core/api/lib/route"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { buildRoute } from "@/api/builders"; import { articleContent } from "@/database/articles"; export const articlesByCategoryRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", path: "/by-category/{categoryId}", @@ -121,14 +119,11 @@ The handler does three things: Put the route in a module and add the module to your API plugin: ```ts title="plugins/example/src/api/modules/articles/articles.module.ts" -import { buildModule } from "@vitnode/core/api/lib/module"; - -import { CONFIG_PLUGIN } from "@/const"; +import { buildModule } from "@/api/builders"; import { articlesByCategoryRoute } from "./by-category.route"; export const articlesModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "articles", routes: [articlesByCategoryRoute], }); @@ -224,7 +219,6 @@ await c.get("db").transaction(async tx => { That is the `editorialService` (available with [`editorial`](/docs/content-engine/editorial)). It needs one extra input, `expectedVersion`: the version of the article the button's page loaded. If somebody saved the article in the meantime, the versions differ and the request fails with `409` instead of silently overwriting their work. ```ts title="plugins/example/src/api/modules/admin/routes/feature.route.ts" -import { buildRoute } from "@vitnode/core/api/lib/route"; import { contentEditorialEffects, resolveContentActor, @@ -233,11 +227,11 @@ import { import { HTTPException } from "hono/http-exception"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { buildRoute } from "@/api/builders"; import { articleContent } from "@/database/articles"; +import { manifest } from "@/manifest.gen"; export const featureArticleRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "article", permission: "can_edit" }, route: { method: "post", @@ -271,7 +265,7 @@ export const featureArticleRoute = buildRoute({ const { id } = c.req.valid("param"); const { expectedVersion } = c.req.valid("json"); - const pluginId = CONFIG_PLUGIN.pluginId; + const pluginId = manifest.id; const outcome = await withHttpErrors( "update", @@ -345,7 +339,7 @@ import { contentVisibilityEffects, resolveContentActor } from "@vitnode/core/con const { editorialService } = postContent; if (!editorialService) throw new Error("Articles have no editorial."); -const pluginId = CONFIG_PLUGIN.pluginId; +const pluginId = manifest.id; const outcome = await editorialService(c, { pluginId }).hide(703, { actor: resolveContentActor(c), }); @@ -364,17 +358,15 @@ Service methods take and return ids typed by the content type's [`idStrategy`](/ A custom route reads an id from the URL as text. Parse it with the content type's strategy before calling a service, so a malformed id answers `400` instead of reaching the database. The example plugin's events use bigint ids: ```ts title="plugins/example/src/api/modules/events/title.route.ts" -import { buildRoute } from "@vitnode/core/api/lib/route"; import { parseContentIdOf } from "@vitnode/core/content"; import { HTTPException } from "hono/http-exception"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { buildRoute } from "@/api/builders"; import { eventContentType } from "@/content/event"; import { eventContent } from "@/database/events"; export const eventTitleRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", path: "/{id}/title", diff --git a/apps/web/content/docs/content-engine/threads.mdx b/apps/web/content/docs/content-engine/threads.mdx index 17dae170f3..ebc7052b77 100644 --- a/apps/web/content/docs/content-engine/threads.mdx +++ b/apps/web/content/docs/content-engine/threads.mdx @@ -132,11 +132,11 @@ The table gets a `parent` column with a foreign key to `forum_comments.id` and a ```ts title="plugins/forum/src/pages/thread-data.ts" import { fetcher } from "@vitnode/core/tanstack/fetcher"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; export const fetchTopicThread = async (topicId: number, after?: string) => { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/comments", path: "/thread", diff --git a/apps/web/content/docs/dev/advanced/queue.mdx b/apps/web/content/docs/dev/advanced/queue.mdx index 94b28f101d..b8eed76735 100644 --- a/apps/web/content/docs/dev/advanced/queue.mdx +++ b/apps/web/content/docs/dev/advanced/queue.mdx @@ -34,7 +34,7 @@ export const sendNewsletterTask = buildQueueTask({ Attach the task to your module's `queueTasks` list: ```ts title="plugins/blog/src/api/modules/posts/posts.module.ts" -import { buildModule } from "@vitnode/core/api/lib/module" +import { buildModule } from "@/api/builders" import { sendNewsletterTask } from "../../tasks/send-newsletter.task" export const postsModule = buildModule({ diff --git a/apps/web/content/docs/dev/ai/actions.mdx b/apps/web/content/docs/dev/ai/actions.mdx index 025cbfdee4..125dd01ca4 100644 --- a/apps/web/content/docs/dev/ai/actions.mdx +++ b/apps/web/content/docs/dev/ai/actions.mdx @@ -46,7 +46,7 @@ import { aiActionRef } from '@vitnode/core/api/lib/ai/registry' import { checkStaffPermission } from '@vitnode/core/api/lib/check-staff-permission' import { z } from 'zod' -import { CONFIG_PLUGIN } from '@/const' +import { manifest } from '@/manifest.gen' export const zodSummarizeTopicSchema = z.object({ title: z.string().trim().min(1).max(255), @@ -80,7 +80,7 @@ export const summarizeTopicAiAction = defineAiAction({ authorize: async ({ c }) => await checkStaffPermission(c, { type: 'admin', - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, module: 'topics', permission: 'can_view', }), @@ -89,7 +89,7 @@ export const summarizeTopicAiAction = defineAiAction({ export const forumAiActions = [summarizeTopicAiAction] export const SUMMARIZE_TOPIC_AI_ACTION = aiActionRef( - CONFIG_PLUGIN.pluginId, + manifest.id, summarizeTopicAiAction, ) ``` @@ -133,11 +133,11 @@ import { buildApiPlugin } from '@vitnode/core/api/lib/plugin' import { forumAiActions } from '@/api/ai/actions' import { adminModule } from '@/api/modules/admin/admin.module' -import { CONFIG_PLUGIN } from '@/const' +import { manifest } from '@/manifest.gen' export const forumApiPlugin = () => buildApiPlugin({ - pluginId: CONFIG_PLUGIN.pluginId, + manifest, aiActions: forumAiActions, // [!code ++] modules: [adminModule], }) @@ -150,17 +150,15 @@ export const forumApiPlugin = () => ### Run it from a route ```ts title="plugins/forum/src/api/modules/admin/ai/routes/summarize.route.ts" -import { buildRoute } from '@vitnode/core/api/lib/route' import { z } from 'zod' import { SUMMARIZE_TOPIC_AI_ACTION, zodSummarizeTopicSchema, } from '@/api/ai/actions' -import { CONFIG_PLUGIN } from '@/const' +import { buildRoute } from '@/api/builders' export const summarizeTopicRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: 'topics', permission: 'can_view' }, route: { method: 'post', @@ -277,7 +275,7 @@ export const suggestTagsAiAction = defineAiAction({ }) export const TOPIC_TAGS_AI_ACTION = aiActionRef( - CONFIG_PLUGIN.pluginId, + manifest.id, suggestTagsAiAction, ) ``` diff --git a/apps/web/content/docs/dev/ai/fields-and-editor.mdx b/apps/web/content/docs/dev/ai/fields-and-editor.mdx index 82b56dc569..8fa967cac2 100644 --- a/apps/web/content/docs/dev/ai/fields-and-editor.mdx +++ b/apps/web/content/docs/dev/ai/fields-and-editor.mdx @@ -40,8 +40,8 @@ import { defineAiAction } from '@vitnode/core/api/lib/ai/action' import { checkStaffPermission } from '@vitnode/core/api/lib/check-staff-permission' import { z } from 'zod' -import { CONFIG_PLUGIN } from '@/const' import { articleContentType } from '@/content/article' +import { manifest } from '@/manifest.gen' export const articleExcerptAiAction = defineAiAction({ id: 'article.excerpt', @@ -77,7 +77,7 @@ export const articleExcerptAiAction = defineAiAction({ await checkStaffPermission(c, { module: articleContentType.permissionModule, permission: 'can_edit', - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, type: 'admin', }), }) @@ -94,7 +94,7 @@ The real file turns the locale code into a language name with `Intl.DisplayNames ```ts title="plugins/example/src/config.api.ts" export const exampleApiPlugin = () => buildApiPlugin({ - pluginId: CONFIG_PLUGIN.pluginId, + manifest, aiActions: [articleExcerptAiAction], // [!code ++] modules: [adminModule /* ... */], }) diff --git a/apps/web/content/docs/dev/ai/usage.mdx b/apps/web/content/docs/dev/ai/usage.mdx index ac2f982c49..ef37947dcd 100644 --- a/apps/web/content/docs/dev/ai/usage.mdx +++ b/apps/web/content/docs/dev/ai/usage.mdx @@ -42,12 +42,12 @@ VitNode uses the [Vercel AI SDK](https://ai-sdk.dev). Resolve a model from `c.ge Generate text in a Hono API route: ```ts title="plugins/blog/src/api/modules/posts/routes/summarize.route.ts" -import { buildRoute } from '@vitnode/core/api/lib/route' import { generateText } from 'ai' import { z } from 'zod' +import { buildRoute } from '@/api/builders' + export const summarizeRoute = buildRoute({ - pluginId: 'blog', route: { method: 'post', path: '/summarize', diff --git a/apps/web/content/docs/dev/captcha/custom-adapter.mdx b/apps/web/content/docs/dev/captcha/custom-adapter.mdx index 4f5d97a9a2..494cc7e1cb 100644 --- a/apps/web/content/docs/dev/captcha/custom-adapter.mdx +++ b/apps/web/content/docs/dev/captcha/custom-adapter.mdx @@ -114,7 +114,6 @@ route does, in a [plugin](/docs/dev/plugins/create) exactly as in core. ```ts title="plugins/my-plugin/src/api/modules/contact/routes/create.route.ts" export const createContactRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: 'post', path: '/', diff --git a/apps/web/content/docs/dev/captcha/index.mdx b/apps/web/content/docs/dev/captcha/index.mdx index 4c50098281..45b0c78187 100644 --- a/apps/web/content/docs/dev/captcha/index.mdx +++ b/apps/web/content/docs/dev/captcha/index.mdx @@ -45,10 +45,9 @@ Built-in user registration and password recovery routes are protected immediatel Enable captcha verification on any custom Hono endpoint with `withCaptcha: true`: ```ts title="plugins/blog/src/api/modules/comments/routes/create.route.ts" -import { buildRoute } from "@vitnode/core/api/lib/route" +import { buildRoute } from "@/api/builders" export const createCommentRoute = buildRoute({ - pluginId: "blog", withCaptcha: true, // [!code ++] route: { method: "post", diff --git a/apps/web/content/docs/dev/configuration.mdx b/apps/web/content/docs/dev/configuration.mdx index 492e492bad..8e1a29e808 100644 --- a/apps/web/content/docs/dev/configuration.mdx +++ b/apps/web/content/docs/dev/configuration.mdx @@ -137,6 +137,7 @@ of those modules, and that is the part worth knowing: | `src/content-registry.gen.ts` | the plugin's `admin/content` | behind a dynamic `import()` in `src/router.tsx` | | `src/package-messages.gen.ts` | the factory's `localeFiles` | per request, one locale at a time | | `src/api-registry.gen.ts` | the plugin's `config.api` | never - type-only, it teaches the fetcher which plugins exist | +| `src/plugin-inventory.gen.ts` | the factory's `metadata` | by the API only, through `buildApiConfig({ frontendPlugins })` | One literal import per configured plugin, written at build time. So a content type's editing screen - a Tiptap field, a form layout, a table cell - arrives @@ -144,7 +145,8 @@ with the route that renders it, not with the config that the document shell imports. Spreading `admin/content` into the factory undoes exactly that: the whole editing stack rides along with every public page. See [Plugin routes](/docs/dev/routing) and -[Plugin frontend modules](/docs/content-engine/plugin-files). +[Plugin frontend modules](/docs/content-engine/plugin-files). The inventory is +what [the AdminCP plugin catalog](/docs/dev/plugins/catalog) lists. The `messages` a factory carries is the plugin's own locale barrel, which loads @@ -158,7 +160,7 @@ whole editing stack rides along with every public page. See Your Vite build executes this file with `jiti` on every regeneration pass to discover the plugin list. Running a plugin's registration graph in Node - measures ~340ms per pass against ~6ms for `buildPlugin({pluginId})`, which is + measures ~340ms per pass against ~6ms for `buildPlugin({ manifest })`, which is the minimum a TanStack host needs - worth knowing if a large plugin set makes the dev watcher feel slow. diff --git a/apps/web/content/docs/dev/cron/index.mdx b/apps/web/content/docs/dev/cron/index.mdx index e411860ea2..5989789fa1 100644 --- a/apps/web/content/docs/dev/cron/index.mdx +++ b/apps/web/content/docs/dev/cron/index.mdx @@ -34,7 +34,7 @@ export const cleanupCron = buildCron({ Attach the cron job to a module in `src/api/modules/`: ```ts title="plugins/blog/src/api/modules/posts/posts.module.ts" -import { buildModule } from '@vitnode/core/api/lib/module' +import { buildModule } from '@/api/builders' import { cleanupCron } from '../../cron/cleanup.cron' export const postsModule = buildModule({ diff --git a/apps/web/content/docs/dev/database/index.mdx b/apps/web/content/docs/dev/database/index.mdx index 944eadcfbb..6861244ab4 100644 --- a/apps/web/content/docs/dev/database/index.mdx +++ b/apps/web/content/docs/dev/database/index.mdx @@ -33,11 +33,10 @@ export const reactions = camelCase.table.withRLS("reactions", (t) => ({ Access the typed Drizzle instance using `c.get("db")`: ```ts title="plugins/reactions/src/api/modules/reactions/routes/get.route.ts" -import { buildRoute } from "@vitnode/core/api/lib/route" +import { buildRoute } from "@/api/builders" import { reactions } from "@/database/reactions" export const getReactionsRoute = buildRoute({ - pluginId: "reactions", route: { method: "get", path: "/", diff --git a/apps/web/content/docs/dev/database/pagination.mdx b/apps/web/content/docs/dev/database/pagination.mdx index d9984f7c83..7ada2f0c2b 100644 --- a/apps/web/content/docs/dev/database/pagination.mdx +++ b/apps/web/content/docs/dev/database/pagination.mdx @@ -28,7 +28,6 @@ import { import { blog_posts } from '@/database/posts' export const getPostsRoute = buildRoute({ - pluginId: 'blog', route: { method: 'get', path: '/', diff --git a/apps/web/content/docs/dev/debugging/logging.mdx b/apps/web/content/docs/dev/debugging/logging.mdx index 7dbffb25a5..eb91a9839e 100644 --- a/apps/web/content/docs/dev/debugging/logging.mdx +++ b/apps/web/content/docs/dev/debugging/logging.mdx @@ -17,12 +17,9 @@ server's stdout. No setup, no adapter, no configuration. middleware, a cron job, a queue task, an event listener: ```ts title="src/api/modules/orders/routes/refund.route.ts" -import { buildRoute } from '@vitnode/core/api/lib/route' - -import { CONFIG_PLUGIN } from '@/const' +import { buildRoute } from '@/api/builders' export const refundOrderRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: 'post', path: '/refund' }, handler: async (c) => { await c.get('log').warn('Refund requested with no matching payment') // [!code ++] diff --git a/apps/web/content/docs/dev/email/index.mdx b/apps/web/content/docs/dev/email/index.mdx index 6272fda51e..29397455c8 100644 --- a/apps/web/content/docs/dev/email/index.mdx +++ b/apps/web/content/docs/dev/email/index.mdx @@ -14,10 +14,9 @@ VitNode provides a unified transactional email service accessible via `c.get("em ### Queue an email from a Hono route ```ts title="plugins/blog/src/api/modules/newsletter/routes/welcome.route.ts" -import { buildRoute } from '@vitnode/core/api/lib/route' +import { buildRoute } from '@/api/builders' export const sendWelcomeRoute = buildRoute({ - pluginId: 'blog', route: { method: 'post', path: '/welcome', diff --git a/apps/web/content/docs/dev/events/index.mdx b/apps/web/content/docs/dev/events/index.mdx index 022f9e70e8..1c7777faaf 100644 --- a/apps/web/content/docs/dev/events/index.mdx +++ b/apps/web/content/docs/dev/events/index.mdx @@ -47,7 +47,7 @@ export const welcomeListener = buildEventListener({ Register the listener in your module's `events` array: ```ts title="plugins/shop/src/api/modules/orders/orders.module.ts" -import { buildModule } from "@vitnode/core/api/lib/module" +import { buildModule } from "@/api/builders" import { welcomeListener } from "../../lib/events" export const ordersModule = buildModule({ diff --git a/apps/web/content/docs/dev/i18n/server.mdx b/apps/web/content/docs/dev/i18n/server.mdx index 3fc1ae22d2..b63720197e 100644 --- a/apps/web/content/docs/dev/i18n/server.mdx +++ b/apps/web/content/docs/dev/i18n/server.mdx @@ -156,7 +156,7 @@ import messages from './locales' export const yourPlugin = () => buildPlugin({ - pluginId: CONFIG_PLUGIN.pluginId, + manifest, localeFiles: { // [!code ++:2] en: '@acme/your-plugin/locales/en.json', @@ -184,7 +184,7 @@ import messages from './locales/api' // [!code ++] export const yourApiPlugin = () => buildApiPlugin({ - pluginId: CONFIG_PLUGIN.pluginId, + manifest, messages, // [!code ++] modules: [postsModule], }) diff --git a/apps/web/content/docs/dev/moderation/enable-reports.mdx b/apps/web/content/docs/dev/moderation/enable-reports.mdx index 7050c41292..b7a9d83cc3 100644 --- a/apps/web/content/docs/dev/moderation/enable-reports.mdx +++ b/apps/web/content/docs/dev/moderation/enable-reports.mdx @@ -83,7 +83,7 @@ import { ReportButton } from "@vitnode/core/tanstack/moderation"; {!metadata.hidden && metadata.itemId !== null ? ( buildApiPlugin({ - pluginId: CONFIG_PLUGIN.pluginId, + manifest, messages, notificationTypes: [topicReplyNotification], // [!code ++] modules: [topicsModule], diff --git a/apps/web/content/docs/dev/plugins/admin-plugins-catalog.webp b/apps/web/content/docs/dev/plugins/admin-plugins-catalog.webp new file mode 100644 index 0000000000..901e3b81c7 Binary files /dev/null and b/apps/web/content/docs/dev/plugins/admin-plugins-catalog.webp differ diff --git a/apps/web/content/docs/dev/plugins/api/modules.mdx b/apps/web/content/docs/dev/plugins/api/modules.mdx index eda613a172..254dd44ed6 100644 --- a/apps/web/content/docs/dev/plugins/api/modules.mdx +++ b/apps/web/content/docs/dev/plugins/api/modules.mdx @@ -23,12 +23,24 @@ place to describe them. ### Define one plugin endpoint +Route and module files import their builders from the plugin's own +`src/api/builders.ts`, which binds them to the generated `manifest`. That is +where the plugin id comes from, so no route or module spells it: + +```ts title="plugins/site-notes/src/api/builders.ts" +import { bindApiPlugin } from '@vitnode/core/api/lib/bind-api-plugin' + +import { manifest } from '@/manifest.gen' + +export const { buildModule, buildRoute } = bindApiPlugin(manifest) +``` + ```ts title="plugins/site-notes/src/api/modules/notes/list.route.ts" import { z } from '@hono/zod-openapi' -import { buildRoute } from '@vitnode/core/api/lib/route' + +import { buildRoute } from '@/api/builders' export const listNotesRoute = buildRoute({ - pluginId: '@acme/site-notes', route: { method: 'get', path: '/', @@ -54,13 +66,12 @@ export const listNotesRoute = buildRoute({ ### Group it and export the plugin API ```ts title="plugins/site-notes/src/api/modules/notes/notes.module.ts" -import { buildModule } from '@vitnode/core/api/lib/module' +import { buildModule } from '@/api/builders' import { listNotesRoute } from './list.route' export const notesModule = buildModule({ name: 'notes', - pluginId: '@acme/site-notes', routes: [listNotesRoute], // [!code ++] }) ``` @@ -69,11 +80,12 @@ export const notesModule = buildModule({ import { buildApiPlugin } from '@vitnode/core/api/lib/plugin' import { notesModule } from './api/modules/notes/notes.module' +import { manifest } from './manifest.gen' export const siteNotesApiPlugin = () => buildApiPlugin({ + manifest, modules: [notesModule], // [!code ++] - pluginId: '@acme/site-notes', }) ``` @@ -101,7 +113,7 @@ import type { ApiPluginContract } from '@vitnode/core/api/lib/plugin' import { buildApiPlugin } from '@vitnode/core/api/lib/plugin' export const siteNotesApiPlugin = () => - buildApiPlugin({ modules: [notesModule], pluginId: '@acme/site-notes' }) + buildApiPlugin({ manifest, modules: [notesModule] }) // [!code ++:3] export type VitNodeApiPlugin = ApiPluginContract< diff --git a/apps/web/content/docs/dev/plugins/api/routes.mdx b/apps/web/content/docs/dev/plugins/api/routes.mdx index ddb7324e82..4bc9f2d20b 100644 --- a/apps/web/content/docs/dev/plugins/api/routes.mdx +++ b/apps/web/content/docs/dev/plugins/api/routes.mdx @@ -17,10 +17,10 @@ OpenAPI record travel together. ```ts title="plugins/site-notes/src/api/modules/notes/get.route.ts" import { z } from '@hono/zod-openapi' -import { buildRoute } from '@vitnode/core/api/lib/route' + +import { buildRoute } from '@/api/builders' export const getNoteRoute = buildRoute({ - pluginId: '@acme/site-notes', route: { method: 'get', path: '/{id}', @@ -56,15 +56,15 @@ still authorizes server-side; a hidden button is merely good manners. ```ts title="plugins/site-notes/src/api/modules/notes/publish.route.ts" import { z } from '@hono/zod-openapi' -import { buildRoute } from '@vitnode/core/api/lib/route' + +import { buildRoute } from '@/api/builders' export const publishNoteRoute = buildRoute({ - // [!code ++:13] + // [!code ++:4] adminStaffPermission: { module: 'site_notes', permission: 'can_publish', }, - pluginId: '@acme/site-notes', route: { method: 'post', path: '/{id}/publish', diff --git a/apps/web/content/docs/dev/plugins/build.mdx b/apps/web/content/docs/dev/plugins/build.mdx index f71cabf0c3..28013e7205 100644 --- a/apps/web/content/docs/dev/plugins/build.mdx +++ b/apps/web/content/docs/dev/plugins/build.mdx @@ -76,6 +76,10 @@ Declarations are written from every file `tsconfig.build.json` includes. That ma You don't need `isolatedDeclarations` or explicit return types. Inferred types come out the same as `tsc` would write them. +## Generated manifest + +Before compiling, `vitnode build` also writes `src/manifest.gen.ts`. It exports `manifest`, which holds the plugin's `name` (as `id`), `version`, `description`, `author` and `license` from `package.json`, and imports the title and Lucide icon from the plugin's own `src/plugin-info.ts`. `vitnode dev` writes it on start and again whenever `package.json` changes. It is the single source of the plugin id, passed to `buildPlugin()`, `buildApiPlugin()` and `bindApiPlugin()`, and it is what the [AdminCP plugin catalog](/docs/dev/plugins/catalog) shows, so commit it with the rest of `src`. + ## What a plugin package needs | File | Why | diff --git a/apps/web/content/docs/dev/plugins/catalog.mdx b/apps/web/content/docs/dev/plugins/catalog.mdx new file mode 100644 index 0000000000..fccafdf553 --- /dev/null +++ b/apps/web/content/docs/dev/plugins/catalog.mdx @@ -0,0 +1,160 @@ +--- +title: Show a plugin in the AdminCP catalog +description: Give your plugin a title and a Lucide icon so AdminCP → System → Plugins lists it with the version that is actually installed. +icon: Puzzle +--- + +import { ImgDocs } from '@/components/fumadocs/img' + +import catalog from './admin-plugins-catalog.webp' + +**AdminCP → System → Plugins** lists every plugin the running application is configured with: its icon, title, package ID, description and installed version, with the VitNode core version shown separately above the list. The version and description come from each plugin's own `package.json` at build time, so nobody types a version number by hand. The title and icon come from a small file in the plugin. + + + +The catalog only reports what is installed. It does not check for updates, install or remove plugins, or switch them on and off. One thing at a time. + +## Add your plugin to the catalog + + + + +### Describe the plugin in package.json + +Make sure your `package.json` has a `description` next to its `name` and `version`: + +```json title="plugins/site-notes/package.json" +{ + "name": "@acme/site-notes", + "version": "1.2.0", + "description": "Pin short notes to any page of your community." +} +``` + + + + +### Set the title and icon + +Create `src/plugin-info.ts` with the title and a [Lucide icon](https://lucide.dev/icons) name: + +```ts title="src/plugin-info.ts" +import type { PluginInfo } from '@vitnode/core/lib/plugin-manifest' + +export const pluginInfo: PluginInfo = { + title: 'Site Notes', + icon: 'sticky-note', +} +``` + +Plugins created with the generator already have this file, with the `puzzle` icon. + + + + +### Register the generated manifest + +`vitnode build` writes `src/manifest.gen.ts`. It holds the `name`, `version`, `description`, `author` and `license` from `package.json` and imports the title and icon from `src/plugin-info.ts`. It is also the single source of the plugin id (`manifest.id`), so nothing else spells the id by hand. Pass it to both registrations: + +```tsx title="src/config.tsx" +import { buildPlugin } from '@vitnode/core/lib/plugin' + +import { manifest } from './manifest.gen' +import { routes } from './routes' + +export const siteNotesPlugin = () => + buildPlugin({ + manifest, + routes, + }) +``` + +```ts title="src/config.api.ts" +import { buildApiPlugin } from '@vitnode/core/api/lib/plugin' + +import { manifest } from '@/manifest.gen' + +export const siteNotesApiPlugin = () => + buildApiPlugin({ + manifest, + modules: [], + }) +``` + + + + +### Build the plugin + +Run the build in the plugin's folder, or keep `vitnode dev` running: + +```bash +vitnode build +``` + +Both commands generate the file before TypeScript and tsdown run. Commit it, like `types/api-registry.gen.d.ts`. + + + + +### Check the result + +Restart your app, sign in to the AdminCP and open **System → Plugins**. Your plugin shows its icon, title, `@acme/site-notes`, the description and `1.2.0`. Search matches the title, the package ID and the description. + +## Where each value comes from + +| Value | Source | Required | Default | +| ----------- | ------------------------------- | -------- | -------------- | +| ID | `name` in `package.json` | Yes | - | +| Version | `version` in `package.json` | No | Unknown | +| Description | `description` in `package.json` | No | No description | +| Title | `title` in `src/plugin-info.ts` | No | The ID | +| Icon | `icon` in `src/plugin-info.ts` | No | Puzzle icon | + +The ID is the plugin id: `buildPlugin()` and `buildApiPlugin()` read it from `manifest.id`. Title and description are plain text, shown as written in every language. The icon is a Lucide icon name in kebab-case, such as `newspaper` or `messages-square`. `buildPlugin()` and `buildApiPlugin()` throw at startup for anything else, and an unknown name falls back to the puzzle icon. Because an icon is just a name, the API returns it too, so plugins registered only in the API show their own icon as well. + +## When the manifest is regenerated + +- `vitnode build` regenerates the file before every build, so a published package always carries its real version. +- `vitnode dev` regenerates it whenever `package.json` changes, or `src/plugin-info.ts` is created or deleted. Edits inside `src/plugin-info.ts` need no regeneration: the generated file imports it. The file is only rewritten when its content changes, so adding a dependency rebuilds nothing. +- A broken `package.json` edit is reported in the terminal and the last good file stays in place. + +The app picks up a new plugin, a removed one or a new version after it is rebuilt and restarted. Nothing is stored in the database. + +## Frontend and API inventory + +A plugin can be registered in `vitnode.config.ts` (frontend), in `vitnode.api.config.ts` (API), or both. The catalog lists each plugin ID once. The API cannot see the frontend config, so the web build hands it over: the Vite plugin writes `src/plugin-inventory.gen.ts`, a plain list of `{ id, manifest }` entries with no plugin code, and you pass it to the API config: + +```ts title="src/vitnode.api.config.ts" +import { buildApiConfig } from '@vitnode/core/vitnode.config' + +import { frontendPlugins } from './plugin-inventory.gen' + +export const vitNodeApiConfig = buildApiConfig({ + plugins: [siteNotesApiPlugin()], + frontendPlugins, + // ...the rest of your API config +}) +``` + +New apps that serve their API from the web app already do this. If your API runs as a separate deployment without that web build, leave `frontendPlugins` out. The catalog then lists the API's plugins only and says so with a notice, instead of guessing what the browser runs. + +## Version mismatch warnings + +When the web build and the API both report a version for the same plugin and the versions differ, the row shows a warning such as _"Version mismatch: the web app runs 1.1.0, the API runs 1.2.0."_ It usually means one side was redeployed without the other. Rebuild and redeploy both. + +A side that reports no version is unknown, not a mismatch. + +## Older plugins + +`buildPlugin()` and `buildApiPlugin()` take the plugin id from `manifest`, so an older plugin that still passes `pluginId` needs `src/manifest.gen.ts` first: run `vitnode build` or `vitnode dev` once, then pass `manifest` instead. A plugin without `src/plugin-info.ts` gets its ID as the title and the default icon. Add it when convenient. + +A frontend inventory entry carrying the manifest of another package throws at startup: the API checks that `manifest.id` equals the entry's `id`. + +## Who can see the catalog + +The page and its endpoint, `GET /api/@vitnode/core/admin/plugins`, require the **Plugins → View installed plugins and their versions** permission (`plugins.can_view` in the `@vitnode/core` module). Staff without it don't see the link, get a not-found page on a direct visit, and a `403` from the API. Root administrators have it automatically. diff --git a/apps/web/content/docs/dev/plugins/create.mdx b/apps/web/content/docs/dev/plugins/create.mdx index 0fbcc466fc..2157d9e5d8 100644 --- a/apps/web/content/docs/dev/plugins/create.mdx +++ b/apps/web/content/docs/dev/plugins/create.mdx @@ -92,10 +92,13 @@ import generatedPageImage from './generated-page-image.png' - - - - + + + + + + + @@ -106,7 +109,7 @@ import generatedPageImage from './generated-page-image.png' - + @@ -116,7 +119,7 @@ import generatedPageImage from './generated-page-image.png' -One page at `/site-notes` whose loader calls the plugin's own `hello` endpoint, its strings, and a Vitest test for the endpoint (`pnpm test` in the plugin). The generator also registers the plugin in your apps' `vitnode.config.ts` and `vitnode.api.config.ts`, installs it and builds it. +One page at `/site-notes` whose loader calls the plugin's own `hello` endpoint, its strings, and a Vitest test for the endpoint (`pnpm test` in the plugin). `src/plugin-info.ts` sets the plugin's title and Lucide icon for [the AdminCP plugin catalog](/docs/dev/plugins/catalog), and `src/api/builders.ts` binds `buildModule` and `buildRoute` to the plugin's generated `manifest`, so API files never spell the plugin id. The generator also registers the plugin in your apps' `vitnode.config.ts` and `vitnode.api.config.ts`, installs it and builds it. The plugin name must be a valid npm name and must not be one of core's routes: `admin`, `api`, `core`, `discover`, `files`, `login`, `notifications`, `register`, `search`, `users` or `vitnode`. diff --git a/apps/web/content/docs/dev/plugins/meta.json b/apps/web/content/docs/dev/plugins/meta.json index bb71ded73a..3acabd884f 100644 --- a/apps/web/content/docs/dev/plugins/meta.json +++ b/apps/web/content/docs/dev/plugins/meta.json @@ -6,6 +6,7 @@ "pages": [ "create", "build", + "catalog", "api", "admin", "navigation", diff --git a/apps/web/content/docs/dev/plugins/navigation.mdx b/apps/web/content/docs/dev/plugins/navigation.mdx index 98d243d6e8..fc5b7ecf67 100644 --- a/apps/web/content/docs/dev/plugins/navigation.mdx +++ b/apps/web/content/docs/dev/plugins/navigation.mdx @@ -32,9 +32,11 @@ all a preset needs; `isOpenInNewTab` is optional and off by default. ```ts title="plugins/site-notes/src/config.api.ts" import { buildApiPlugin } from '@vitnode/core/api/lib/plugin' +import { manifest } from '@/manifest.gen' + export const siteNotesApiPlugin = () => buildApiPlugin({ - pluginId: '@acme/site-notes', + manifest, // [!code ++:9] navigation: [ { href: '/notes', icon: 'notebook-pen', id: 'notes' }, diff --git a/apps/web/content/docs/dev/search.mdx b/apps/web/content/docs/dev/search.mdx index a1944bef71..a20b72d5ec 100644 --- a/apps/web/content/docs/dev/search.mdx +++ b/apps/web/content/docs/dev/search.mdx @@ -89,7 +89,7 @@ Register it in `config.api.ts`: ```ts title="plugins/blog/src/config.api.ts" export const blogApiPlugin = () => buildApiPlugin({ - pluginId: 'blog', + manifest, searchIndexers: [postSearchIndexer], // [!code ++] }) ``` diff --git a/apps/web/content/docs/dev/storage/index.mdx b/apps/web/content/docs/dev/storage/index.mdx index 5a29d7d262..0fca1a9831 100644 --- a/apps/web/content/docs/dev/storage/index.mdx +++ b/apps/web/content/docs/dev/storage/index.mdx @@ -31,10 +31,10 @@ Handle uploads in a Hono route using `c.get("storage").upload()`: ```ts title="plugins/blog/src/api/modules/posts/routes/upload-cover.route.ts" import { z } from "@hono/zod-openapi" -import { buildRoute } from "@vitnode/core/api/lib/route" + +import { buildRoute } from "@/api/builders" export const uploadCoverRoute = buildRoute({ - pluginId: "blog", route: { method: "post", path: "/cover", diff --git a/apps/web/content/docs/dev/swagger.mdx b/apps/web/content/docs/dev/swagger.mdx index bfb718e86e..327205aded 100644 --- a/apps/web/content/docs/dev/swagger.mdx +++ b/apps/web/content/docs/dev/swagger.mdx @@ -59,12 +59,10 @@ spec people ignore: ```ts title="src/api/modules/orders/routes/show.route.ts" import { z } from '@hono/zod-openapi' -import { buildRoute } from '@vitnode/core/api/lib/route' -import { CONFIG_PLUGIN } from '@/const' +import { buildRoute } from '@/api/builders' export const showOrderRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: 'get', path: '/{id}', @@ -95,13 +93,11 @@ export const showOrderRoute = buildRoute({ ### Register it in a module ```ts title="src/api/modules/orders/orders.module.ts" -import { buildModule } from '@vitnode/core/api/lib/module' +import { buildModule } from '@/api/builders' -import { CONFIG_PLUGIN } from '@/const' import { showOrderRoute } from './routes/show.route' export const ordersModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: 'orders', // [!code ++] routes: [showOrderRoute], // [!code ++] }) @@ -119,11 +115,11 @@ read in the sidebar. import { buildApiPlugin } from '@vitnode/core/api/lib/plugin' import { ordersModule } from '@/api/modules/orders/orders.module' -import { CONFIG_PLUGIN } from '@/const' +import { manifest } from '@/manifest.gen' export const shopApiPlugin = () => buildApiPlugin({ - pluginId: CONFIG_PLUGIN.pluginId, + manifest, modules: [ordersModule], // [!code ++] }) ``` diff --git a/apps/web/content/docs/dev/widgets/content-zones.mdx b/apps/web/content/docs/dev/widgets/content-zones.mdx index 91fbf364a8..f250e83f74 100644 --- a/apps/web/content/docs/dev/widgets/content-zones.mdx +++ b/apps/web/content/docs/dev/widgets/content-zones.mdx @@ -446,7 +446,7 @@ In `plugins/example/src/blocks.tsx`, comment the callout out of the registered l ```ts export const blocks = { - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, blocks: [], // was [calloutBlock] } satisfies BlockPluginSource ``` diff --git a/apps/web/content/docs/dev/widgets/editable-pages.mdx b/apps/web/content/docs/dev/widgets/editable-pages.mdx index 41f5d47f59..7b509686b6 100644 --- a/apps/web/content/docs/dev/widgets/editable-pages.mdx +++ b/apps/web/content/docs/dev/widgets/editable-pages.mdx @@ -94,7 +94,7 @@ The definition module is a plain value with **no third-party import anywhere in ```ts title="plugins/example/src/config.api.ts" buildApiPlugin({ - pluginId: CONFIG_PLUGIN.pluginId, + manifest, // [!code ++:4] editablePages: [settingsPage], permissionStaff: { diff --git a/apps/web/content/docs/dev/widgets/end-to-end.mdx b/apps/web/content/docs/dev/widgets/end-to-end.mdx index dc2f57bdcc..36e761b3e0 100644 --- a/apps/web/content/docs/dev/widgets/end-to-end.mdx +++ b/apps/web/content/docs/dev/widgets/end-to-end.mdx @@ -71,12 +71,12 @@ The subpath is the registration. Name the file `src/blocks.tsx` so it resolves a ```tsx title="plugins/example/src/blocks.tsx" import type { BlockPluginSource } from '@vitnode/core/widgets' -import { CONFIG_PLUGIN } from '@/const' +import { manifest } from '@/manifest.gen' import { calloutBlock } from './blocks/callout' export const blocks = { - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, blocks: [calloutBlock], } satisfies BlockPluginSource ``` @@ -95,14 +95,14 @@ The API validates writes, so it needs the definitions too. Pass the **same objec ```ts title="plugins/example/src/config.api.ts" import { buildApiPlugin } from '@vitnode/core/api/lib/plugin' -import { CONFIG_PLUGIN } from '@/const' +import { manifest } from '@/manifest.gen' // [!code ++:2] import { blocks } from './blocks' export const exampleApiPlugin = () => buildApiPlugin({ - pluginId: CONFIG_PLUGIN.pluginId, + manifest, // [!code ++:2] blocks, modules: [adminModule], diff --git a/apps/web/content/docs/dev/working-with-users/staff-permissions.mdx b/apps/web/content/docs/dev/working-with-users/staff-permissions.mdx index faa931901b..e920b0838d 100644 --- a/apps/web/content/docs/dev/working-with-users/staff-permissions.mdx +++ b/apps/web/content/docs/dev/working-with-users/staff-permissions.mdx @@ -17,9 +17,11 @@ VitNode separates staff into two groups: **Moderators** and **Administrators**. ```ts title="plugins/blog/src/config.api.ts" import { buildApiPlugin } from "@vitnode/core/api/lib/plugin" +import { manifest } from "@/manifest.gen" + export const blogApiPlugin = () => buildApiPlugin({ - pluginId: "@vitnode/blog", + manifest, // [!code ++:8] permissionStaff: { admin: { @@ -38,7 +40,6 @@ Add `adminStaffPermission` to automatically return `403 Forbidden` if unauthoriz ```ts title="plugins/blog/src/api/modules/admin/posts/routes/delete.route.ts" export const deletePostRoute = buildRoute({ - pluginId: "@vitnode/blog", // [!code ++:1] adminStaffPermission: { module: "posts", permission: "can_delete" }, route: { diff --git a/apps/web/content/docs/ui/data-table/draggable-rows.mdx b/apps/web/content/docs/ui/data-table/draggable-rows.mdx index 166929d935..9b0e4ab70f 100644 --- a/apps/web/content/docs/ui/data-table/draggable-rows.mdx +++ b/apps/web/content/docs/ui/data-table/draggable-rows.mdx @@ -114,7 +114,6 @@ always need a staff permission. ```ts title="plugins/tasks/src/api/modules/admin/tasks/routes/reorder.route.ts" export const reorderTasksRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, // [!code ++] adminStaffPermission: { module: 'tasks', permission: 'can_reorder' }, route: { diff --git a/apps/web/src/locales/@vitnode/core/pl.json b/apps/web/src/locales/@vitnode/core/pl.json index b03b3518a0..38441dbe71 100644 --- a/apps/web/src/locales/@vitnode/core/pl.json +++ b/apps/web/src/locales/@vitnode/core/pl.json @@ -27,6 +27,8 @@ "@vitnode/core:ai": "AI", "@vitnode/core:ai:can_view": "Wyświetlanie użycia, ustawień i historii AI", "@vitnode/core:ai:can_manage": "Zarządzanie ustawieniami, modelami, cenami i dostępem AI", + "@vitnode/core:plugins": "Wtyczki", + "@vitnode/core:plugins:can_view": "Przeglądanie zainstalowanych wtyczek i ich wersji", "@vitnode/core:queue": "Zadania w kolejce", "@vitnode/core:queue:can_view": "Wyświetlanie zadań w kolejce", "@vitnode/core:roles": "Role", @@ -310,6 +312,7 @@ "files": "Pliki", "integrations": "Integracje", "navigation": "Nawigacja", + "plugins": "Wtyczki", "title": "System" }, "user_bar": { @@ -850,6 +853,27 @@ "desc": "Aktualizacje na żywo i powiadomienia wysyłane do połączonych klientów w czasie rzeczywistym.", "title": "WebSocket" } + }, + "plugins": { + "desc": "Wszystkie wtyczki skonfigurowane w tej aplikacji i wersje, których używa.", + "empty": { + "desc": "Dodaj wtyczkę do vitnode.config.ts lub vitnode.api.config.ts, a pojawi się tutaj po następnym buildzie.", + "title": "Brak zainstalowanych wtyczek" + }, + "list": { + "plugin": "Wtyczka", + "version": "Wersja" + }, + "mismatch": "Niezgodne wersje: aplikacja webowa używa {frontend}, a API {api}. Przebuduj i wdróż oba tak, aby były zgodne.", + "no_description": "Brak opisu.", + "no_results": { + "clear": "Wyczyść wyszukiwanie", + "desc": "Spróbuj nazwy wtyczki, jej ID pakietu lub słowa z opisu.", + "title": "Żadna wtyczka nie pasuje do wyszukiwania" + }, + "search": "Szukaj wtyczek...", + "title": "Wtyczki", + "version_unknown": "Nieznana wersja" } }, "user": { diff --git a/apps/web/src/page-cache.gen.ts b/apps/web/src/page-cache.gen.ts index c48778d544..ac0163d4b3 100644 --- a/apps/web/src/page-cache.gen.ts +++ b/apps/web/src/page-cache.gen.ts @@ -34,6 +34,7 @@ export const pageCacheRoutes: PageCacheRouteTable = [ { path: '/admin/core/system/integrations' }, { path: '/admin/core/system/navigation' }, { path: '/admin/core/system/notifications' }, + { path: '/admin/core/system/plugins' }, { path: '/admin/core/users/roles' }, { path: '/admin/core/users/:id' }, { path: '/admin/core/ai' }, diff --git a/apps/web/src/plugin-inventory.gen.ts b/apps/web/src/plugin-inventory.gen.ts new file mode 100644 index 0000000000..51ed5ffbeb --- /dev/null +++ b/apps/web/src/plugin-inventory.gen.ts @@ -0,0 +1,68 @@ +/* oxlint-disable */ + +// This file is generated by VitNode. Do not edit it, and do not format it. +// +// It is rewritten by the `vitnode:plugin-routes` Vite plugin on every +// `vite dev` and `vite build`, from the plugins configured in +// `src/vitnode.config.ts` and the manifest each one registers. +// +// Plain data and nothing else: no plugin code is imported, so the API can read +// it without loading the frontend. Pass it to +// `buildApiConfig({ frontendPlugins })` so AdminCP → System → Plugins lists +// what this web build runs, frontend-only plugins included. +// +// Same plugin configuration in, same bytes out: the entries are sorted by +// plugin id. + +import type { FrontendPluginInventory } from '@vitnode/core/lib/plugin-manifest' + +export const frontendPlugins: FrontendPluginInventory = [ + { + id: "@vitnode/blog", + manifest: { + id: "@vitnode/blog", + title: "Blog", + icon: "newspaper", + description: "Blog plugin for VitNode, providing a blogging platform on Hono.js.", + author: { + name: "VitNode", + email: "contact@vitnode.com", + url: "https://vitnode.com", + }, + license: "MIT", + version: "2.0.0-canary.13", + }, + }, + { + id: "@vitnode/example", + manifest: { + id: "@vitnode/example", + title: "Example", + icon: "flask-conical", + description: "Reference plugin exercising the VitNode Content Engine end to end.", + author: { + name: "VitNode", + email: "contact@vitnode.com", + url: "https://vitnode.com", + }, + license: "MIT", + version: "2.0.0-canary.13", + }, + }, + { + id: "@vitnode/forum", + manifest: { + id: "@vitnode/forum", + title: "Forum", + icon: "messages-square", + description: "Community forum for VitNode: structured forums and nested threads, built on the Content Engine.", + author: { + name: "VitNode", + email: "contact@vitnode.com", + url: "https://vitnode.com", + }, + license: "MIT", + version: "2.0.0-canary.13", + }, + }, +] diff --git a/apps/web/src/vitnode.api.config.ts b/apps/web/src/vitnode.api.config.ts index e6f55309f9..2a955190fa 100644 --- a/apps/web/src/vitnode.api.config.ts +++ b/apps/web/src/vitnode.api.config.ts @@ -9,6 +9,7 @@ import { drizzle } from 'drizzle-orm/postgres-js' import { contentModules } from './content-modules.gen' import { appMessages } from './locales/app' +import { frontendPlugins } from './plugin-inventory.gen' import { vitNodeConfig } from './vitnode.config' config({ quiet: true }) @@ -29,6 +30,7 @@ export const POSTGRES_URL = export const vitNodeApiConfig = buildApiConfig({ plugins: [blogApiPlugin(), exampleApiPlugin(), forumApiPlugin()], contentModules, + frontendPlugins, storage: { image: { quality: 85, diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/api-single-app/src/vitnode.api.config.ts b/packages/create-vitnode-app/copy-of-vitnode-app/api-single-app/src/vitnode.api.config.ts index 5add4731ae..888d2c3158 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/api-single-app/src/vitnode.api.config.ts +++ b/packages/create-vitnode-app/copy-of-vitnode-app/api-single-app/src/vitnode.api.config.ts @@ -3,6 +3,7 @@ import { buildApiConfig } from "@vitnode/core/vitnode.config"; import { drizzle } from "drizzle-orm/postgres-js"; import { contentModules } from "./content-modules.gen"; +import { frontendPlugins } from "./plugin-inventory.gen"; import { vitNodeConfig } from "./vitnode.config"; export const POSTGRES_URL = @@ -15,6 +16,7 @@ export const vitNodeApiConfig = buildApiConfig({ }, plugins: [], contentModules, + frontendPlugins, i18n: vitNodeConfig.i18n, dbProvider: drizzle({ diff --git a/packages/create-vitnode-app/src/helpers/packages-json.ts b/packages/create-vitnode-app/src/helpers/packages-json.ts index 42eb943416..fb975bbd31 100644 --- a/packages/create-vitnode-app/src/helpers/packages-json.ts +++ b/packages/create-vitnode-app/src/helpers/packages-json.ts @@ -1,5 +1,6 @@ export interface PackageJSON { dependencies?: Record; + description?: string; devDependencies?: Record; engines?: Record; diff --git a/packages/create-vitnode-app/src/plugin/create/create-package-json.ts b/packages/create-vitnode-app/src/plugin/create/create-package-json.ts index 4950e1e07a..e08fbfb1e9 100644 --- a/packages/create-vitnode-app/src/plugin/create/create-package-json.ts +++ b/packages/create-vitnode-app/src/plugin/create/create-package-json.ts @@ -36,6 +36,7 @@ export const createPluginPackageJSON = async ({ const pluginPkg: PackageJSON = { name: pluginName, version: "0.1.0", + description: "A VitNode plugin.", private: true, type: "module", engines: { node: supportedNodeRange }, diff --git a/packages/create-vitnode-app/src/plugin/create/route-templates.test.ts b/packages/create-vitnode-app/src/plugin/create/route-templates.test.ts index 12905f48f4..1fa45060a5 100644 --- a/packages/create-vitnode-app/src/plugin/create/route-templates.test.ts +++ b/packages/create-vitnode-app/src/plugin/create/route-templates.test.ts @@ -3,13 +3,13 @@ import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { + pluginApiBuildersTemplate, pluginApiConfigTemplate, pluginApiModuleTemplate, pluginApiModuleTestTemplate, pluginApiRouteTemplate, pluginApiVariableName, pluginConfigTemplate, - pluginConstTemplate, pluginGlobalTypesTemplate, pluginMessagesTemplate, pluginPackageExports, @@ -94,14 +94,14 @@ describe("the generated route tree", () => { match => match[1], ); - expect(imports).toEqual(["@vitnode/core/routing", "./const"]); + expect(imports).toEqual(["@vitnode/core/routing", "./manifest.gen"]); expect(imports).not.toContain("./pages/home-page"); expect(routes).toContain("lazy(() => import("); }); /** * The namespace is built by the *generated* plugin at runtime, from its own - * `CONFIG_PLUGIN`. Written into a template literal inside a template literal, + * manifest. Written into a template literal inside a template literal, * so its backticks and `${}` have to survive generation rather than be * evaluated by it - the difference between a plugin that compiles and one * whose `routes.ts` says `messages: [".home"]`. @@ -109,7 +109,7 @@ describe("the generated route tree", () => { it("leaves the message namespace for the generated plugin to build", () => { const routes = pluginRoutesTemplate("@acme/blog"); - expect(routes).toContain("messages: [`${CONFIG_PLUGIN.pluginId}.home`],"); + expect(routes).toContain("messages: [`${manifest.id}.home`],"); expect(routes).not.toContain("@acme/blog.home"); }); @@ -145,7 +145,7 @@ describe("the generated route module", () => { "@vitnode/core/routing", "@vitnode/core/tanstack/fetcher", "use-intl", - "@/const", + "@/manifest.gen", ]); expect(imports).not.toContain("@tanstack/react-router"); }); @@ -205,11 +205,11 @@ describe("the generated config", () => { expect(config).toContain("routes,"); }); - it("names the plugin through the one constant that holds its id", () => { + it("names the plugin through its generated manifest", () => { const config = pluginConfigTemplate("@acme/blog"); - expect(config).toContain('import { CONFIG_PLUGIN } from "@/const";'); - expect(config).toContain("pluginId: CONFIG_PLUGIN.pluginId,"); + expect(config).toContain('import { manifest } from "./manifest.gen";'); + expect(config).toContain("manifest,"); expect(config).not.toContain('"@acme/blog"'); }); @@ -220,21 +220,20 @@ describe("the generated config", () => { }); }); -describe("the generated constant", () => { - it("holds the plugin's id as a literal, which the fetcher infers from", () => { - const constants = pluginConstTemplate("@acme/blog"); - - expect(constants).toContain('pluginId: "@acme/blog" as const,'); +describe("the generated builders", () => { + it("binds the API builders to the generated manifest", () => { + expect(pluginApiBuildersTemplate()).toContain( + "export const { buildModule, buildRoute } = bindApiPlugin(manifest);", + ); }); - it("is the only generated file that spells the plugin's id out", () => { + it("leaves the plugin's id to the generated manifest", () => { const files = pluginRouteScaffold("@acme/blog"); Object.entries(files) .filter( ([file]) => file !== "global.d.ts" && - file !== "src/const.ts" && file !== "src/locales/en.json" && file !== "src/pages/home-page.tsx" && file !== "src/routes.ts", @@ -422,7 +421,7 @@ describe("the scaffold as a whole", () => { "src/api/modules/hello/hello.route.ts", ); expect(Object.keys(files)).toContain("src/config.api.ts"); - expect(Object.keys(files)).toContain("src/const.ts"); + expect(Object.keys(files)).toContain("src/api/builders.ts"); expect(Object.keys(files)).toContain("global.d.ts"); expect(Object.keys(files)).not.toContain("src/api/client.ts"); }); @@ -485,9 +484,7 @@ describe("the generated endpoint test", () => { expect(test).toContain('helloModule.hono.request("/")'); expect(test).toContain('message: "Hello from @acme/blog!"'); // The same greeting the route answers with. - expect(pluginApiRouteTemplate()).toContain( - "Hello from ${CONFIG_PLUGIN.pluginId}!", - ); + expect(pluginApiRouteTemplate()).toContain("Hello from ${manifest.id}!"); }); }); diff --git a/packages/create-vitnode-app/src/plugin/create/route-templates.ts b/packages/create-vitnode-app/src/plugin/create/route-templates.ts index 4c986aa30d..4b1f911c96 100644 --- a/packages/create-vitnode-app/src/plugin/create/route-templates.ts +++ b/packages/create-vitnode-app/src/plugin/create/route-templates.ts @@ -3,23 +3,41 @@ export const routeSlugFor = (pluginName: string): string => ? pluginName.slice(pluginName.indexOf("/") + 1) : pluginName; -export const pluginConstTemplate = (pluginName: string): string => - `export const CONFIG_PLUGIN = { - pluginId: "${pluginName}" as const, +export const pluginTitleFor = (pluginName: string): string => + routeSlugFor(pluginName) + .split(/[^A-Za-z0-9]+/) + .filter(Boolean) + .map(part => `${part[0].toUpperCase()}${part.slice(1)}`) + .join(" ") || pluginName; + +export const pluginInfoTemplate = (pluginName: string): string => + `import type { PluginInfo } from "@vitnode/core/lib/plugin-manifest"; + +export const pluginInfo: PluginInfo = { + title: ${JSON.stringify(pluginTitleFor(pluginName))}, + icon: "puzzle", }; `; +export const pluginApiBuildersTemplate = (): string => + `import { bindApiPlugin } from "@vitnode/core/api/lib/bind-api-plugin"; + +import { manifest } from "@/manifest.gen"; + +export const { buildModule, buildRoute } = bindApiPlugin(manifest); +`; + export const pluginRoutesTemplate = (pluginName: string): string => { const slug = routeSlugFor(pluginName); return `import { definePluginRoutes, lazy, page } from "@vitnode/core/routing"; -import { CONFIG_PLUGIN } from "./const"; +import { manifest } from "./manifest.gen"; export const routes = definePluginRoutes([ page("/${slug}", { component: lazy(() => import("./pages/home-page")), - messages: [\`\${CONFIG_PLUGIN.pluginId}.home\`], + messages: [\`\${manifest.id}.home\`], }), ]); `; @@ -39,7 +57,7 @@ import { definePluginRoute } from "@vitnode/core/routing"; import { fetcher } from "@vitnode/core/tanstack/fetcher"; import { useTranslations } from "use-intl"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; interface HelloMessage { message: string; @@ -48,7 +66,7 @@ interface HelloMessage { export const route = definePluginRoute({ load: async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "hello", path: "/", @@ -151,11 +169,15 @@ export const pluginVariableName = (pluginName: string): string => { /** * `src/config.tsx` - what an application registers. * - * The routes, the locale files and the messages, and nothing else. `routes` is - * the same tree `routes.ts` exports, handed on unchanged: an app on Vite reads - * that file directly at build time and an app that registers the plugin the - * ordinary way reads it through here, so the two paths cannot describe - * different routes. + * The routes, the locale files, the messages and the plugin manifest, and + * nothing else. `manifest.gen.ts` is written by `vitnode build` and + * `vitnode dev` from `package.json` and `src/plugin-info.ts`, so the + * version the AdminCP shows is never typed by hand. + * + * `routes` is the same tree `routes.ts` exports, handed on unchanged: an app on + * Vite reads that file directly at build time and an app that registers the + * plugin the ordinary way reads it through here, so the two paths cannot + * describe different routes. * * `localeFiles` is the same list as the barrel above it, spelled as specifiers * rather than as loaders: an app's build writes its own loaders from it, and a @@ -164,14 +186,13 @@ export const pluginVariableName = (pluginName: string): string => { export const pluginConfigTemplate = (pluginName: string): string => `import { buildPlugin } from "@vitnode/core/lib/plugin"; -import { CONFIG_PLUGIN } from "@/const"; - import messages from "./locales"; +import { manifest } from "./manifest.gen"; import { routes } from "./routes"; export const ${pluginVariableName(pluginName)} = () => buildPlugin({ - pluginId: CONFIG_PLUGIN.pluginId, + manifest, localeFiles: { en: "${pluginName}/locales/en.json", }, @@ -185,12 +206,11 @@ export const pluginApiVariableName = (pluginName: string): string => export const pluginApiRouteTemplate = (): string => `import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@vitnode/core/api/lib/route"; -import { CONFIG_PLUGIN } from "@/const"; +import { buildRoute } from "@/api/builders"; +import { manifest } from "@/manifest.gen"; export const helloRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", path: "/", @@ -205,19 +225,16 @@ export const helloRoute = buildRoute({ }, }, }, - handler: c => c.json({ message: \`Hello from \${CONFIG_PLUGIN.pluginId}!\` }), + handler: c => c.json({ message: \`Hello from \${manifest.id}!\` }), }); `; export const pluginApiModuleTemplate = (): string => - `import { buildModule } from "@vitnode/core/api/lib/module"; - -import { CONFIG_PLUGIN } from "@/const"; + `import { buildModule } from "@/api/builders"; import { helloRoute } from "./hello.route"; export const helloModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "hello", routes: [helloRoute], }); @@ -228,13 +245,13 @@ export const pluginApiConfigTemplate = (pluginName: string): string => import { buildApiPlugin } from "@vitnode/core/api/lib/plugin"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { helloModule } from "./api/modules/hello/hello.module"; export const ${pluginApiVariableName(pluginName)} = () => buildApiPlugin({ - pluginId: CONFIG_PLUGIN.pluginId, + manifest, modules: [helloModule], }); @@ -339,15 +356,16 @@ export const pluginRouteScaffold = ( pluginName: string, ): Record => ({ "global.d.ts": pluginGlobalTypesTemplate(), + "src/api/builders.ts": pluginApiBuildersTemplate(), "src/api/modules/hello/hello.module.test.ts": pluginApiModuleTestTemplate(pluginName), "src/api/modules/hello/hello.module.ts": pluginApiModuleTemplate(), "src/api/modules/hello/hello.route.ts": pluginApiRouteTemplate(), "src/config.api.ts": pluginApiConfigTemplate(pluginName), "src/config.tsx": pluginConfigTemplate(pluginName), - "src/const.ts": pluginConstTemplate(pluginName), "src/locales/en.json": pluginMessagesTemplate(pluginName), "src/locales/index.ts": pluginMessagesBarrelTemplate(), + "src/plugin-info.ts": pluginInfoTemplate(pluginName), "src/pages/home-page.tsx": pluginRouteModuleTemplate(pluginName), "src/routes.ts": pluginRoutesTemplate(pluginName), }); diff --git a/packages/vitnode/package.json b/packages/vitnode/package.json index 83af1e8ac5..57a2051b7b 100644 --- a/packages/vitnode/package.json +++ b/packages/vitnode/package.json @@ -2,7 +2,11 @@ "name": "@vitnode/core", "version": "2.0.0-canary.13", "description": "Core package for VitNode, providing essential functionalities and configurations.", - "author": "VitNode Team", + "author": { + "name": "VitNode", + "email": "contact@vitnode.com", + "url": "https://vitnode.com" + }, "license": "MIT", "homepage": "https://vitnode.com", "repository": { diff --git a/packages/vitnode/scripts/cli/builder/compiler-build.ts b/packages/vitnode/scripts/cli/builder/compiler-build.ts index 837e13e3b9..b91a63f1e5 100644 --- a/packages/vitnode/scripts/cli/builder/compiler-build.ts +++ b/packages/vitnode/scripts/cli/builder/compiler-build.ts @@ -7,7 +7,11 @@ import type { MeasuredFile } from "./output-files"; import type { TsdownApi } from "./package-build"; import { writePluginApiRegistry } from "../../write-plugin-api-registry"; -import { errorMessage, RuntimeError } from "../errors"; +import { + definesPlugin, + writePluginManifest, +} from "../../write-plugin-manifest"; +import { ConfigError, errorMessage, RuntimeError } from "../errors"; import { importFromProject, resolveBin } from "../project/packages"; import { runProcess } from "../project/processes"; import { captureProcessOutput } from "../ui/capture-output"; @@ -199,6 +203,17 @@ export const prepareRegistryStep = (project: Project, ui: Ui) => { } }; +export const prepareManifestStep = (project: Project, ui: Ui) => { + if (!definesPlugin(project.root)) return; + + try { + writePluginManifest(project.root); + } catch (error) { + throw new ConfigError(errorMessage(error), { cause: error }); + } + ui.success("Plugin manifest generated"); +}; + /** Every file under `dir`, as measured output, for the compiler builds. */ export const listOutput = (project: Project, dir: string): MeasuredFile[] => { const root = join(project.root, dir); diff --git a/packages/vitnode/scripts/cli/commands/build.ts b/packages/vitnode/scripts/cli/commands/build.ts index cf6e9c830f..e4744da1e2 100644 --- a/packages/vitnode/scripts/cli/commands/build.ts +++ b/packages/vitnode/scripts/cli/commands/build.ts @@ -11,6 +11,7 @@ import { runAppBuild } from "../builder/app-build"; import { apiBuildSteps, listOutput, + prepareManifestStep, prepareRegistryStep, runCompilerSteps, runPackageBuild, @@ -153,6 +154,7 @@ export const runBuildCommand = async ( if (project.kind === "package") { ui.header(`Building ${project.name}`); if (isPluginPackage(project.root)) prepareRegistryStep(project, ui); + prepareManifestStep(project, ui); await runPackageBuild({ loadTsdown: deps.loadTsdown, project, diff --git a/packages/vitnode/scripts/cli/commands/dev.ts b/packages/vitnode/scripts/cli/commands/dev.ts index 50bfd5539f..b451fe7e8d 100644 --- a/packages/vitnode/scripts/cli/commands/dev.ts +++ b/packages/vitnode/scripts/cli/commands/dev.ts @@ -6,6 +6,11 @@ import type { ProcessGroup } from "../project/processes"; import type { Project } from "../project/project"; import { writePluginApiRegistry } from "../../write-plugin-api-registry"; +import { + definesPlugin, + watchPluginManifest, + writePluginManifest, +} from "../../write-plugin-manifest"; import { createDatabaseServices } from "../db/database"; import { prepareDevelopmentDatabase } from "../db/prepare"; import { openUrl } from "../dev/open-url"; @@ -16,6 +21,7 @@ import { runWatchers, toProcess, } from "../dev/watchers"; +import { ConfigError, errorMessage } from "../errors"; import { loadConfiguredPluginIds } from "../plugins/discover"; import { resolveFromProject } from "../project/packages"; import { detectProject } from "../project/project"; @@ -43,6 +49,27 @@ const apiHost = (host: DevOptions["host"]): string | undefined => const ownsDatabase = (project: Project) => project.drizzleConfig !== null && project.hasApi; +const watchManifest = (root: string, ui: CommandContext["ui"]) => { + let inputs: string[]; + try { + inputs = writePluginManifest(root).inputs; + } catch (error) { + throw new ConfigError(errorMessage(error), { cause: error }); + } + ui.success("Plugin manifest generated"); + + return watchPluginManifest({ + inputs, + onChange: () => { + ui.success("Plugin manifest regenerated from package.json"); + }, + onError: error => { + ui.error(errorMessage(error)); + }, + root, + }); +}; + /** * `vitnode dev` - the development environment for whatever this folder is. * @@ -66,17 +93,26 @@ export const runDevCommand = async ( resolveFromProject(project.root, "tsdown"); if (writePluginApiRegistry(project.root)) ui.success("API registry generated"); + const manifest = definesPlugin(project.root) + ? watchManifest(project.root, ui) + : null; ui.info( "Watching sources - apps using this plugin reload as dist/ changes", ); ui.line(); - return runWatchers({ - group: deps.group, - processes: packageWatchers().map(watcher => toProcess(project, watcher)), - signals, - ui, - }); + try { + return await runWatchers({ + group: deps.group, + processes: packageWatchers().map(watcher => + toProcess(project, watcher), + ), + signals, + ui, + }); + } finally { + manifest?.close(); + } } ui.header("Development"); diff --git a/packages/vitnode/scripts/cli/plugins/validate.ts b/packages/vitnode/scripts/cli/plugins/validate.ts index 6a96a10648..35442698a0 100644 --- a/packages/vitnode/scripts/cli/plugins/validate.ts +++ b/packages/vitnode/scripts/cli/plugins/validate.ts @@ -213,7 +213,7 @@ export const validatePlugin = async ( definition = findDefinition(await importModule(configFile)); if (definition === null) { fail("Plugin definition", [ - `${show(configFile)} exports no factory returning buildPlugin({ pluginId, ... }).`, + `${show(configFile)} exports no factory returning buildPlugin({ manifest, ... }).`, ]); } else { pluginsFromLoadedConfig( diff --git a/packages/vitnode/scripts/watch-plugin-manifest.test.ts b/packages/vitnode/scripts/watch-plugin-manifest.test.ts new file mode 100644 index 0000000000..499bc6bc09 --- /dev/null +++ b/packages/vitnode/scripts/watch-plugin-manifest.test.ts @@ -0,0 +1,115 @@ +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { + PLUGIN_INFO_PATH, + PLUGIN_MANIFEST_PATH, +} from "../src/framework/plugin-manifest/generate.js"; +import { + watchPluginManifest, + writePluginManifest, +} from "./write-plugin-manifest.js"; + +const BLOG = { name: "@acme/blog", version: "1.0.0" }; + +let root: string; +let close: () => void = () => undefined; + +const write = (path: string, content: string) => { + writeFileSync(join(root, path), content); +}; + +const read = (path: string) => readFileSync(join(root, path), "utf8"); + +const settle = async () => + await new Promise(resolve => setTimeout(resolve, 400)); + +const watch = async () => { + const onChange = vi.fn(); + const onError = vi.fn(); + const { inputs } = writePluginManifest(root); + ({ close } = watchPluginManifest({ inputs, onChange, onError, root })); + await settle(); + + return { onChange, onError }; +}; + +beforeEach(() => { + root = mkdtempSync(join(tmpdir(), "vitnode-plugin-manifest-watch-")); + mkdirSync(join(root, "src")); + write("package.json", JSON.stringify(BLOG)); +}); + +afterEach(() => { + close(); + rmSync(root, { force: true, recursive: true }); +}); + +describe("watchPluginManifest", () => { + it("regenerates when package.json changes and when src/plugin-info.ts comes and goes", async () => { + const { onChange, onError } = await watch(); + const importsInfo = () => read(PLUGIN_MANIFEST_PATH).includes("pluginInfo"); + + write("package.json", JSON.stringify({ ...BLOG, version: "2.0.0" })); + await vi.waitFor( + () => { + expect(read(PLUGIN_MANIFEST_PATH)).toContain('"2.0.0"'); + }, + { timeout: 5000 }, + ); + await settle(); + + write(PLUGIN_INFO_PATH, 'export const pluginInfo = { icon: "newspaper" };'); + await vi.waitFor( + () => { + expect(importsInfo()).toBe(true); + }, + { timeout: 5000 }, + ); + await settle(); + + rmSync(join(root, PLUGIN_INFO_PATH)); + await vi.waitFor( + () => { + expect(importsInfo()).toBe(false); + }, + { timeout: 5000 }, + ); + + expect(onChange).toHaveBeenCalledTimes(3); + expect(onError).not.toHaveBeenCalled(); + }); + + it("reports a broken edit, keeps the last file, and recovers", async () => { + const { onChange, onError } = await watch(); + const metadata = read(PLUGIN_MANIFEST_PATH); + + write("package.json", "{ broken"); + await vi.waitFor( + () => { + expect(onError).toHaveBeenCalled(); + }, + { timeout: 5000 }, + ); + expect(read(PLUGIN_MANIFEST_PATH)).toBe(metadata); + expect(onChange).not.toHaveBeenCalled(); + await settle(); + + write("package.json", JSON.stringify({ ...BLOG, version: "2.0.0" })); + await vi.waitFor( + () => { + expect(read(PLUGIN_MANIFEST_PATH)).toContain('"2.0.0"'); + }, + { timeout: 5000 }, + ); + expect(onChange).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/vitnode/scripts/write-plugin-manifest.test.ts b/packages/vitnode/scripts/write-plugin-manifest.test.ts new file mode 100644 index 0000000000..82ae3d46f2 --- /dev/null +++ b/packages/vitnode/scripts/write-plugin-manifest.test.ts @@ -0,0 +1,187 @@ +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; + +import { + PLUGIN_INFO_PATH, + PLUGIN_MANIFEST_PATH, +} from "../src/framework/plugin-manifest/generate.js"; +import { definesPlugin, writePluginManifest } from "./write-plugin-manifest.js"; + +const dirs: string[] = []; + +afterEach(() => { + for (const dir of dirs.splice(0)) { + rmSync(dir, { force: true, recursive: true }); + } +}); + +const tempDir = () => { + const dir = mkdtempSync(join(tmpdir(), "vitnode-plugin-manifest-")); + dirs.push(dir); + + return dir; +}; + +const write = (root: string, path: string, content: string) => { + mkdirSync(dirname(join(root, path)), { recursive: true }); + writeFileSync(join(root, path), content); +}; + +const writeManifest = (root: string, manifest: Record) => { + write(root, "package.json", JSON.stringify(manifest, null, 2)); +}; + +const writeInfo = (root: string, info: Record) => { + write( + root, + PLUGIN_INFO_PATH, + `export const pluginInfo = ${JSON.stringify(info)};\n`, + ); +}; + +const pluginAt = (manifest: Record) => { + const root = tempDir(); + mkdirSync(join(root, "src")); + writeManifest(root, manifest); + + return root; +}; + +const BLOG = { description: "Posts", name: "@acme/blog", version: "1.0.0" }; + +const generated = async (root: string): Promise => { + const module = (await import(join(root, PLUGIN_MANIFEST_PATH))) as { + manifest: unknown; + }; + + return module.manifest; +}; + +describe("writePluginManifest", () => { + it("writes the manifest, and watches src/plugin-info.ts before it exists", () => { + const root = pluginAt(BLOG); + + expect(writePluginManifest(root)).toEqual({ + changed: true, + inputs: [join(root, "package.json"), join(root, PLUGIN_INFO_PATH)], + }); + expect(existsSync(join(root, PLUGIN_MANIFEST_PATH))).toBe(true); + }); + + it.each([ + ["no src/plugin-info.ts", undefined, "@acme/blog", null], + ["an empty plugin info", {}, "@acme/blog", null], + [ + "a title and an icon", + { icon: "newspaper", title: "Blog" }, + "Blog", + "newspaper", + ], + ])("takes the title and icon from %s", async (_, info, title, icon) => { + const root = pluginAt(BLOG); + if (info) writeInfo(root, info); + + writePluginManifest(root); + + expect(await generated(root)).toEqual({ + author: null, + description: "Posts", + icon, + id: "@acme/blog", + license: null, + title, + version: "1.0.0", + }); + }); + + it.each([ + [ + "a new dependency", + (root: string) => { + writeManifest(root, { ...BLOG, dependencies: { zod: "^4.0.0" } }); + }, + false, + ], + [ + "a title edited in src/plugin-info.ts", + (root: string) => { + writeInfo(root, { title: "Journal" }); + }, + false, + ], + [ + "a new version", + (root: string) => { + writeManifest(root, { ...BLOG, version: "1.1.0" }); + }, + true, + ], + [ + "src/plugin-info.ts disappearing", + (root: string) => { + rmSync(join(root, PLUGIN_INFO_PATH)); + }, + true, + ], + ])("after %s, reports changed: %s", (_, edit, changed) => { + const root = pluginAt(BLOG); + writeInfo(root, { title: "Blog" }); + writePluginManifest(root); + const before = readFileSync(join(root, PLUGIN_MANIFEST_PATH), "utf8"); + + edit(root); + + expect(writePluginManifest(root).changed).toBe(changed); + expect( + readFileSync(join(root, PLUGIN_MANIFEST_PATH), "utf8") !== before, + ).toBe(changed); + }); + + it("reports src/plugin-info.ts appearing as a change", () => { + const root = pluginAt(BLOG); + writePluginManifest(root); + + writeInfo(root, { icon: "newspaper" }); + + expect(writePluginManifest(root).changed).toBe(true); + }); + + it("refuses a package without a name and writes nothing", () => { + const root = pluginAt({ version: "1.0.0" }); + + expect(() => writePluginManifest(root)).toThrow(/has no `name`/); + expect(existsSync(join(root, PLUGIN_MANIFEST_PATH))).toBe(false); + }); +}); + +describe("definesPlugin", () => { + it.each([ + [true, "a plugin with src/config.tsx", "@acme/blog", "src/config.tsx"], + [true, "a plugin with src/config.ts", "@acme/blog", "src/config.ts"], + [ + false, + "core, despite its src/config.ts", + "@vitnode/core", + "src/config.ts", + ], + [false, "a package without a config", "@acme/utils", "src/index.ts"], + ])("is %s for %s", (expected, _, name, file) => { + const root = pluginAt({ name }); + write(root, file, "export {};\n"); + + expect(definesPlugin(root)).toBe(expected); + }); + + it("is false for a folder without package.json", () => { + expect(definesPlugin(tempDir())).toBe(false); + }); +}); diff --git a/packages/vitnode/scripts/write-plugin-manifest.ts b/packages/vitnode/scripts/write-plugin-manifest.ts new file mode 100644 index 0000000000..27526cb68f --- /dev/null +++ b/packages/vitnode/scripts/write-plugin-manifest.ts @@ -0,0 +1,107 @@ +import { + existsSync, + readFileSync, + unwatchFile, + watchFile, + writeFileSync, +} from "node:fs"; +import { join, relative } from "node:path"; + +import { + generatePluginManifestSource, + PLUGIN_INFO_PATH, + PLUGIN_MANIFEST_PATH, + pluginManifestFromPackageJson, +} from "../src/framework/plugin-manifest/generate.js"; +import { CORE_PLUGIN_ID } from "../src/framework/plugin-routes/core.js"; + +export interface PluginManifestWrite { + changed: boolean; + inputs: string[]; +} + +const PLUGIN_DEFINITIONS = ["src/config.tsx", "src/config.ts"]; + +export const definesPlugin = (root: string): boolean => { + const manifest = join(root, "package.json"); + if (!existsSync(manifest)) return false; + + const { name } = JSON.parse(readFileSync(manifest, "utf8")) as { + name?: unknown; + }; + + return ( + name !== CORE_PLUGIN_ID && + PLUGIN_DEFINITIONS.some(file => existsSync(join(root, file))) + ); +}; + +export const writePluginManifest = ( + root: string = process.cwd(), +): PluginManifestWrite => { + const manifestPath = join(root, "package.json"); + const infoPath = join(root, PLUGIN_INFO_PATH); + const source = relative(process.cwd(), manifestPath) || "package.json"; + const fields = pluginManifestFromPackageJson( + JSON.parse(readFileSync(manifestPath, "utf8")) as unknown, + source, + ); + const path = join(root, PLUGIN_MANIFEST_PATH); + const output = generatePluginManifestSource(fields, { + hasInfo: existsSync(infoPath), + }); + const changed = !existsSync(path) || readFileSync(path, "utf8") !== output; + + if (changed) writeFileSync(path, output, "utf8"); + + return { changed, inputs: [manifestPath, infoPath] }; +}; + +const POLL_MS = 300; + +export const watchPluginManifest = ({ + inputs, + onChange, + onError, + root, +}: { + inputs: readonly string[]; + onChange: () => void; + onError: (error: unknown) => void; + root: string; +}): { close: () => void } => { + let watched: string[] = []; + + const listener = () => { + try { + const result = writePluginManifest(root); + watch(result.inputs); + if (result.changed) onChange(); + } catch (error) { + onError(error); + } + }; + + const watch = (next: readonly string[]) => { + for (const file of watched) { + if (!next.includes(file)) unwatchFile(file, listener); + } + for (const file of next) { + if (!watched.includes(file)) { + watchFile(file, { interval: POLL_MS }, listener); + } + } + watched = [...next]; + }; + + watch(inputs); + + return { + close: () => { + watched.forEach(file => { + unwatchFile(file, listener); + }); + watched = []; + }, + }; +}; diff --git a/packages/vitnode/src/api/builders.ts b/packages/vitnode/src/api/builders.ts new file mode 100644 index 0000000000..253a8c9bd2 --- /dev/null +++ b/packages/vitnode/src/api/builders.ts @@ -0,0 +1,5 @@ +import { manifest } from "@/manifest"; + +import { bindApiPlugin } from "./lib/bind-api-plugin"; + +export const { buildModule, buildRoute } = bindApiPlugin(manifest); diff --git a/packages/vitnode/src/api/config.ts b/packages/vitnode/src/api/config.ts index cebd41abe5..6a4d84e023 100644 --- a/packages/vitnode/src/api/config.ts +++ b/packages/vitnode/src/api/config.ts @@ -20,8 +20,8 @@ import { createBlockRegistry, setDefaultBlockRegistry, } from "@/blocks/registry"; -import { CONFIG_PLUGIN } from "@/config"; import { CONFIG } from "@/lib/config"; +import { manifest } from "@/manifest"; import { configurePageCacheStore, pageCacheRuntime, @@ -125,7 +125,7 @@ export function VitNodeAPI({ app.doc("/swagger/doc", { openapi: "3.0.0", info: { - version: CONFIG_PLUGIN.version, + version: manifest.version, title: "VitNode API", }, tags: plugins.flatMap( @@ -195,6 +195,7 @@ export function VitNodeAPI({ content: vitNodeApiConfig.content, cron: vitNodeApiConfig.cron, events: vitNodeApiConfig.events, + frontendPlugins: vitNodeApiConfig.frontendPlugins, notifications: vitNodeApiConfig.notifications, search: vitNodeApiConfig.search, storage: vitNodeApiConfig.storage, diff --git a/packages/vitnode/src/api/lib/ai/alt-actions.ts b/packages/vitnode/src/api/lib/ai/alt-actions.ts index 4bf7e0c60e..53178086fa 100644 --- a/packages/vitnode/src/api/lib/ai/alt-actions.ts +++ b/packages/vitnode/src/api/lib/ai/alt-actions.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import { defineAiAction } from "./action"; import { languageName } from "./language-name"; @@ -102,10 +102,10 @@ export const altTranslateAiAction = defineAiAction({ }); export const ALT_GENERATE_AI_ACTION = aiActionRef( - CONFIG_PLUGIN.pluginId, + manifest.id, altGenerateAiAction, ); export const ALT_TRANSLATE_AI_ACTION = aiActionRef( - CONFIG_PLUGIN.pluginId, + manifest.id, altTranslateAiAction, ); diff --git a/packages/vitnode/src/api/lib/ai/editor-actions.ts b/packages/vitnode/src/api/lib/ai/editor-actions.ts index 3ad81a2698..d37e40d267 100644 --- a/packages/vitnode/src/api/lib/ai/editor-actions.ts +++ b/packages/vitnode/src/api/lib/ai/editor-actions.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import { defineAiAction } from "./action"; import { languageName } from "./language-name"; @@ -162,10 +162,10 @@ export const editorQuickAskAiAction = defineAiAction({ }); export const EDITOR_REWRITE_AI_ACTION = aiActionRef( - CONFIG_PLUGIN.pluginId, + manifest.id, editorRewriteAiAction, ); export const EDITOR_QUICK_ASK_AI_ACTION = aiActionRef( - CONFIG_PLUGIN.pluginId, + manifest.id, editorQuickAskAiAction, ); diff --git a/packages/vitnode/src/api/lib/ai/registry.test.ts b/packages/vitnode/src/api/lib/ai/registry.test.ts index 2361769f2b..031cf4abec 100644 --- a/packages/vitnode/src/api/lib/ai/registry.test.ts +++ b/packages/vitnode/src/api/lib/ai/registry.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from "vitest"; import { z } from "zod"; +import { testManifest } from "@/tests/manifest"; + import { buildApiPlugin } from "../plugin"; import { AiActionDefinitionError, defineAiAction } from "./action"; import { collectAiActions, UnknownAiActionError } from "./registry"; @@ -157,7 +159,7 @@ describe("AI action registry", () => { expect(() => buildApiPlugin({ aiActions: [summarize(), summarize()], - pluginId: "@acme/blog", + manifest: testManifest("@acme/blog"), }), ).toThrow(AiActionDefinitionError); }); diff --git a/packages/vitnode/src/api/lib/bind-api-plugin.ts b/packages/vitnode/src/api/lib/bind-api-plugin.ts new file mode 100644 index 0000000000..e294bec4c9 --- /dev/null +++ b/packages/vitnode/src/api/lib/bind-api-plugin.ts @@ -0,0 +1,31 @@ +import type { PluginManifest } from "@/lib/plugin-manifest"; + +import type { + BaseBuildModuleReturn, + BuildModuleOptions, + BuildModuleReturn, +} from "./module"; +import type { BuildRouteConfig, BuildRouteOptions, Route } from "./route"; + +import { buildModule } from "./module"; +import { buildRoute } from "./route"; + +export const bindApiPlugin = ( + manifest: Pick, "id">, +) => ({ + buildModule: < + const M extends string, + const Routes extends Route[], + Modules extends BaseBuildModuleReturn[], + >( + options: BuildModuleOptions, + ): BuildModuleReturn => + buildModule({ + ...options, + pluginId: manifest.id, + }), + buildRoute:

>( + options: BuildRouteOptions, + ): Route => + buildRoute({ ...options, pluginId: manifest.id }), +}); diff --git a/packages/vitnode/src/api/lib/content-modules.integration.test.ts b/packages/vitnode/src/api/lib/content-modules.integration.test.ts index 89546eeae1..e507d83cc9 100644 --- a/packages/vitnode/src/api/lib/content-modules.integration.test.ts +++ b/packages/vitnode/src/api/lib/content-modules.integration.test.ts @@ -16,6 +16,7 @@ import { } from "@/framework/vite/plugin-routes"; import { definePluginRoutes, lazy, page } from "@/routing/tree"; import { testDeliveredPostContentType } from "@/tests/content-fixtures"; +import { testManifest } from "@/tests/manifest"; import { assertPluginContentModules, @@ -130,7 +131,7 @@ const webBuild = async ( const publishingPlugin = () => buildApiPlugin({ - pluginId: PLUGIN_ID, + manifest: testManifest(PLUGIN_ID), modules: [ buildContentPublicModule({ pluginId: PLUGIN_ID, @@ -248,7 +249,7 @@ describe("a plugin's public content URLs, from web build to API boot", () => { expect(() => assertPluginContentModules({ loader: packageContentModules(appRoot), - plugins: [buildApiPlugin({ pluginId: PLUGIN_ID })], + plugins: [buildApiPlugin({ manifest: testManifest(PLUGIN_ID) })], }), ).not.toThrow(); }); diff --git a/packages/vitnode/src/api/lib/installed-plugins.test.ts b/packages/vitnode/src/api/lib/installed-plugins.test.ts new file mode 100644 index 0000000000..a3000ae0fd --- /dev/null +++ b/packages/vitnode/src/api/lib/installed-plugins.test.ts @@ -0,0 +1,187 @@ +import { describe, expect, it } from "vitest"; + +import type { PluginManifest } from "@/lib/plugin-manifest"; + +import { buildInstalledPluginRegistry } from "./installed-plugins"; + +const manifest = ( + id: string, + overrides: Partial = {}, +): PluginManifest => ({ + author: null, + description: `${id} description`, + icon: null, + id, + license: null, + title: `${id} title`, + version: "1.0.0", + ...overrides, +}); + +describe("buildInstalledPluginRegistry", () => { + it("lists each plugin once, sorted by id, without core", () => { + const registry = buildInstalledPluginRegistry({ + api: [ + { manifest: manifest("@acme/shop"), pluginId: "@acme/shop" }, + { pluginId: "@vitnode/core" }, + { manifest: manifest("@acme/blog"), pluginId: "@acme/blog" }, + ], + frontend: [ + { id: "@vitnode/core", manifest: null }, + { id: "@acme/blog", manifest: manifest("@acme/blog") }, + { id: "@acme/analytics", manifest: manifest("@acme/analytics") }, + ], + }); + + expect(registry.plugins.map(plugin => plugin.id)).toEqual([ + "@acme/analytics", + "@acme/blog", + "@acme/shop", + ]); + }); + + it("merges both registrations of a plugin", () => { + const [plugin] = buildInstalledPluginRegistry({ + api: [{ manifest: manifest("@acme/blog"), pluginId: "@acme/blog" }], + frontend: [{ id: "@acme/blog", manifest: manifest("@acme/blog") }], + }).plugins; + + expect(plugin).toEqual({ + description: "@acme/blog description", + icon: null, + id: "@acme/blog", + registrations: { + api: { version: "1.0.0" }, + frontend: { version: "1.0.0" }, + }, + title: "@acme/blog title", + version: "1.0.0", + versionMismatch: false, + }); + }); + + it.each([ + ["the frontend inventory is given", [], true], + ["no frontend inventory is given", undefined, false], + ])("reports whether %s", (_, frontend, expected) => { + expect( + buildInstalledPluginRegistry({ api: [], frontend }).frontendInventory, + ).toBe(expected); + }); + + it.each([ + ["only the API knows it", "2.0.0", null, "2.0.0", false], + ["only the frontend knows it", null, "3.0.0", "3.0.0", false], + ["both agree", "2.0.0", "2.0.0", "2.0.0", false], + ["they differ, preferring the API's", "2.0.0", "3.0.0", "2.0.0", true], + ["neither knows it", null, null, null, false], + ])( + "resolves the version when %s", + (_, apiVersion, frontendVersion, version, versionMismatch) => { + const [plugin] = buildInstalledPluginRegistry({ + api: [ + { + manifest: manifest("@acme/blog", { version: apiVersion }), + pluginId: "@acme/blog", + }, + ], + frontend: [ + { + id: "@acme/blog", + manifest: manifest("@acme/blog", { version: frontendVersion }), + }, + ], + }).plugins; + + expect({ + version: plugin?.version, + versionMismatch: plugin?.versionMismatch, + }).toEqual({ version, versionMismatch }); + }, + ); + + it.each([ + [ + "the API's manifest first", + manifest("@acme/blog", { icon: "rss" }), + "rss", + ], + ["the frontend's when the API has no manifest", undefined, "newspaper"], + ])("takes the icon from %s", (_, apiMetadata, icon) => { + const [plugin] = buildInstalledPluginRegistry({ + api: [{ manifest: apiMetadata, pluginId: "@acme/blog" }], + frontend: [ + { + id: "@acme/blog", + manifest: manifest("@acme/blog", { icon: "newspaper" }), + }, + ], + }).plugins; + + expect(plugin?.icon).toBe(icon); + }); + + it("treats a side without manifest as unknown, not a mismatch", () => { + const [plugin] = buildInstalledPluginRegistry({ + api: [{ pluginId: "@acme/blog" }], + frontend: [{ id: "@acme/blog", manifest: manifest("@acme/blog") }], + }).plugins; + + expect(plugin?.registrations).toEqual({ + api: { version: null }, + frontend: { version: "1.0.0" }, + }); + expect(plugin?.version).toBe("1.0.0"); + expect(plugin?.versionMismatch).toBe(false); + }); + + it("lists a plugin with no manifest anywhere by its id", () => { + const [plugin] = buildInstalledPluginRegistry({ + api: [], + frontend: [{ id: "@acme/blog", manifest: null }], + }).plugins; + + expect(plugin).toEqual({ + description: null, + icon: null, + id: "@acme/blog", + registrations: { api: null, frontend: { version: null } }, + title: "@acme/blog", + version: null, + versionMismatch: false, + }); + }); + + it.each([ + [ + "the API", + { + api: [{ pluginId: "@acme/blog" }, { pluginId: "@acme/blog" }], + frontend: undefined, + }, + ], + [ + "the frontend", + { + api: [], + frontend: [ + { id: "@acme/blog", manifest: null }, + { id: "@acme/blog", manifest: null }, + ], + }, + ], + ])("refuses an id registered twice by %s", (_, input) => { + expect(() => buildInstalledPluginRegistry(input)).toThrow( + /claim the same id: "@acme\/blog"/, + ); + }); + + it("refuses manifest registered under another plugin's id", () => { + expect(() => + buildInstalledPluginRegistry({ + api: [], + frontend: [{ id: "@acme/shop", manifest: manifest("@acme/blog") }], + }), + ).toThrow(/\[VitNode plugin manifest\]/); + }); +}); diff --git a/packages/vitnode/src/api/lib/installed-plugins.ts b/packages/vitnode/src/api/lib/installed-plugins.ts new file mode 100644 index 0000000000..ab2982a7cb --- /dev/null +++ b/packages/vitnode/src/api/lib/installed-plugins.ts @@ -0,0 +1,110 @@ +import type { + FrontendPluginInventory, + PluginManifest, +} from "@/lib/plugin-manifest"; + +import { assertPluginManifest } from "@/lib/plugin-manifest"; +import { manifest as coreManifest } from "@/manifest"; + +export interface InstalledPluginRegistration { + version: null | string; +} + +export interface InstalledPlugin { + description: null | string; + icon: null | string; + id: string; + registrations: { + api: InstalledPluginRegistration | null; + frontend: InstalledPluginRegistration | null; + }; + title: string; + version: null | string; + versionMismatch: boolean; +} + +export interface InstalledPluginRegistry { + frontendInventory: boolean; + plugins: InstalledPlugin[]; +} + +interface Registration { + manifest: null | PluginManifest; +} + +const ERROR_PREFIX = "[VitNode plugins]"; + +const byId = ( + entries: readonly { id: string; manifest: null | PluginManifest }[], + side: string, +): Map => { + const map = new Map(); + + for (const { id, manifest } of entries) { + if (id === coreManifest.id) continue; + + if (map.has(id)) { + throw new Error( + `${ERROR_PREFIX} Two ${side} plugins claim the same id: ${JSON.stringify(id)}. A plugin id is the package name, so an app cannot configure one twice.`, + ); + } + + assertPluginManifest(id, manifest ?? undefined, `The ${side} config`); + map.set(id, { manifest }); + } + + return map; +}; + +const registrationOf = ( + registration: Registration | undefined, +): InstalledPluginRegistration | null => + registration === undefined + ? null + : { version: registration.manifest?.version ?? null }; + +export const buildInstalledPluginRegistry = ({ + api, + frontend, +}: { + api: readonly { manifest?: PluginManifest; pluginId: string }[]; + frontend: FrontendPluginInventory | undefined; +}): InstalledPluginRegistry => { + const apiPlugins = byId( + api.map(plugin => ({ + id: plugin.pluginId, + manifest: plugin.manifest ?? null, + })), + "API", + ); + const frontendPlugins = byId(frontend ?? [], "frontend"); + const ids = [...new Set([...apiPlugins.keys(), ...frontendPlugins.keys()])]; + ids.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); + + return { + frontendInventory: frontend !== undefined, + plugins: ids.map(id => { + const fromApi = apiPlugins.get(id); + const fromFrontend = frontendPlugins.get(id); + const apiVersion = fromApi?.manifest?.version ?? null; + const frontendVersion = fromFrontend?.manifest?.version ?? null; + const manifest = fromApi?.manifest ?? fromFrontend?.manifest ?? null; + + return { + description: manifest?.description ?? null, + icon: manifest?.icon ?? null, + id, + registrations: { + api: registrationOf(fromApi), + frontend: registrationOf(fromFrontend), + }, + title: manifest?.title ?? id, + version: apiVersion ?? frontendVersion, + versionMismatch: + apiVersion !== null && + frontendVersion !== null && + apiVersion !== frontendVersion, + }; + }), + }; +}; diff --git a/packages/vitnode/src/api/lib/module.ts b/packages/vitnode/src/api/lib/module.ts index eb74c6f821..97a0f65bd2 100644 --- a/packages/vitnode/src/api/lib/module.ts +++ b/packages/vitnode/src/api/lib/module.ts @@ -49,6 +49,27 @@ export interface BuildModuleReturn< modules?: Modules; } +export interface BuildModuleOptions< + P extends string, + M extends string, + Routes extends Route

[], + Modules extends BaseBuildModuleReturn

[], +> { + contentContainers?: AnyContentContainerDefinition[]; + contentModels?: AnyContentModel[]; + contentTypes?: AnyContentTypeDefinition[]; + cronJobs?: BuildCronReturn[]; + events?: BuildEventListenerReturn[]; + modules?: Modules; + name: M; + notificationTypes?: AnyNotificationTypeDefinition[]; + publicContentTypes?: AnyContentTypeDefinition[]; + queueTasks?: BuildQueueTaskReturn[]; + routes: Routes; + searchIndexers?: SearchIndexer[]; + webSockets?: BuildWebSocketReturn[]; +} + export function buildModule< const P extends string, const M extends string, @@ -69,21 +90,8 @@ export function buildModule< queueTasks = [], searchIndexers, webSockets = [], -}: { - contentContainers?: AnyContentContainerDefinition[]; - contentModels?: AnyContentModel[]; - contentTypes?: AnyContentTypeDefinition[]; - cronJobs?: BuildCronReturn[]; - events?: BuildEventListenerReturn[]; - modules?: Modules; - name: M; - notificationTypes?: AnyNotificationTypeDefinition[]; +}: BuildModuleOptions & { pluginId: P; - publicContentTypes?: AnyContentTypeDefinition[]; - queueTasks?: BuildQueueTaskReturn[]; - routes: Routes; - searchIndexers?: SearchIndexer[]; - webSockets?: BuildWebSocketReturn[]; }): BuildModuleReturn, M, Routes, Modules> { const hono = new OpenAPIHono(); diff --git a/packages/vitnode/src/api/lib/plugin.test.ts b/packages/vitnode/src/api/lib/plugin.test.ts index 379de31d82..a2ff623cb1 100644 --- a/packages/vitnode/src/api/lib/plugin.test.ts +++ b/packages/vitnode/src/api/lib/plugin.test.ts @@ -8,6 +8,7 @@ import { testDeliveredPostContentType, testSearchablePostContentType, } from "@/tests/content-fixtures"; +import { testManifest } from "@/tests/manifest"; import type { SearchIndexer } from "../models/search"; @@ -32,7 +33,7 @@ const adminModule = buildModule({ describe("buildApiPlugin content types", () => { it("collects content types from nested modules", () => { const plugin = buildApiPlugin({ - pluginId: "@vitnode/example", + manifest: testManifest("@vitnode/example"), modules: [adminModule], }); @@ -44,7 +45,7 @@ describe("buildApiPlugin content types", () => { it("derives staff permissions for each collected content type", () => { const plugin = buildApiPlugin({ - pluginId: "@vitnode/example", + manifest: testManifest("@vitnode/example"), modules: [adminModule], }); @@ -59,7 +60,7 @@ describe("buildApiPlugin content types", () => { it("keeps hand-declared permissions and other modules intact", () => { const plugin = buildApiPlugin({ - pluginId: "@vitnode/example", + manifest: testManifest("@vitnode/example"), modules: [adminModule], permissionStaff: { admin: { article: ["can_view"], posts: ["can_view"] }, @@ -74,7 +75,7 @@ describe("buildApiPlugin content types", () => { it("leaves permissionStaff untouched for a plugin with no content types", () => { const plugin = buildApiPlugin({ - pluginId: "@vitnode/example", + manifest: testManifest("@vitnode/example"), modules: [], }); @@ -91,7 +92,10 @@ describe("buildApiPlugin content types", () => { }); expect(() => - buildApiPlugin({ pluginId: "@vitnode/example", modules: [duplicate] }), + buildApiPlugin({ + manifest: testManifest("@vitnode/example"), + modules: [duplicate], + }), ).toThrow(/Duplicate content type id/); }); }); @@ -111,7 +115,7 @@ describe("buildApiPlugin search indexers", () => { }); const plugin = buildApiPlugin({ - pluginId: "@vitnode/example", + manifest: testManifest("@vitnode/example"), modules: [ buildModule({ pluginId: "@vitnode/example", @@ -129,7 +133,7 @@ describe("buildApiPlugin search indexers", () => { it("merges root-level indexers with collected ones", () => { const plugin = buildApiPlugin({ - pluginId: "@vitnode/example", + manifest: testManifest("@vitnode/example"), modules: [ buildModule({ pluginId: "@vitnode/example", @@ -149,7 +153,7 @@ describe("buildApiPlugin search indexers", () => { it("leaves a plugin with no indexers alone", () => { const plugin = buildApiPlugin({ - pluginId: "@vitnode/example", + manifest: testManifest("@vitnode/example"), modules: [adminModule], }); @@ -159,7 +163,7 @@ describe("buildApiPlugin search indexers", () => { it("rejects the same item type registered twice", () => { expect(() => buildApiPlugin({ - pluginId: "@vitnode/example", + manifest: testManifest("@vitnode/example"), modules: [ buildModule({ pluginId: "@vitnode/example", @@ -180,13 +184,13 @@ describe("validateSearchIndexers", () => { // stamps on a legacy document. const collected = [ ...(buildApiPlugin({ - pluginId: "@vitnode/example", + manifest: testManifest("@vitnode/example"), searchIndexers: [indexer("test.article")], }).searchIndexers ?? []), ].map(item => ({ ...item, pluginId: "@vitnode/example" })); const other = [ ...(buildApiPlugin({ - pluginId: "@vitnode/blog", + manifest: testManifest("@vitnode/blog"), searchIndexers: [indexer("blog_post")], }).searchIndexers ?? []), ].map(item => ({ ...item, pluginId: "@vitnode/blog" })); @@ -238,7 +242,7 @@ describe("buildApiPlugin public content URLs", () => { it("reports every content type that publishes a URL, however it is exposed", () => { const plugin = buildApiPlugin({ - pluginId: PLUGIN_ID, + manifest: testManifest(PLUGIN_ID), modules: [searchableAdmin, deliveredPublic], }); @@ -250,8 +254,10 @@ describe("buildApiPlugin public content URLs", () => { it("reports nothing for content without delivery or search", () => { expect( - buildApiPlugin({ pluginId: PLUGIN_ID, modules: [adminModule] }) - .publicContentTypes, + buildApiPlugin({ + manifest: testManifest(PLUGIN_ID), + modules: [adminModule], + }).publicContentTypes, ).toEqual([]); }); }); diff --git a/packages/vitnode/src/api/lib/plugin.ts b/packages/vitnode/src/api/lib/plugin.ts index a1b441f015..ce5de4405f 100644 --- a/packages/vitnode/src/api/lib/plugin.ts +++ b/packages/vitnode/src/api/lib/plugin.ts @@ -9,6 +9,7 @@ import type { AnyContentTypeDefinition } from "@/content/types"; import type { ApiPluginContract } from "@/lib/fetcher/contract"; import type { LocaleMessagesMap } from "@/lib/i18n/types"; import type { NavigationPresetDeclaration } from "@/lib/navigation"; +import type { PluginManifest } from "@/lib/plugin-manifest"; import { BlockError } from "@/blocks/errors"; import { @@ -24,6 +25,7 @@ import { } from "@/content/registry"; import { contentDefinitionOf } from "@/content/server/model"; import { contentReportTarget } from "@/content/server/report-target"; +import { assertPluginManifest } from "@/lib/plugin-manifest"; import type { SearchIndexer } from "../models/search"; import type { AnyAiActionDefinition } from "./ai/action"; @@ -63,6 +65,7 @@ export interface BuildPluginApiReturn< editablePages?: AnyEditablePageDefinition[]; events?: Omit[]; hono: OpenAPIHono; + manifest?: PluginManifest

; messages?: LocaleMessagesMap; moderation?: Required; modules: Modules; @@ -89,7 +92,7 @@ export function buildApiPlugin< aiActions, blocks, editablePages, - pluginId, + manifest, messages, moderation, modules = [] as unknown as Modules, @@ -101,17 +104,19 @@ export function buildApiPlugin< aiActions?: AnyAiActionDefinition[]; blocks?: BlockPluginSource; editablePages?: AnyEditablePageDefinition[]; + manifest: PluginManifest

; messages?: LocaleMessagesMap; moderation?: ModerationPluginConfig; modules?: Modules; navigation?: NavigationPresetDeclaration[]; notificationTypes?: AnyNotificationTypeDefinition[]; permissionStaff?: PermissionStaffConfig; - pluginId: P; searchIndexers?: SearchIndexer[]; }): BuildPluginApiReturn { + const pluginId = manifest.id; // Run for checking if the plugin is valid checkPluginId(pluginId); + assertPluginManifest(pluginId, manifest, "buildApiPlugin()"); if (blocks && blocks.pluginId !== pluginId) { throw new BlockError( @@ -221,6 +226,7 @@ export function buildApiPlugin< aiActions, blocks, editablePages: registeredPages.map(entry => entry.page), + manifest, messages, modules, navigation, diff --git a/packages/vitnode/src/api/lib/route.ts b/packages/vitnode/src/api/lib/route.ts index 7e32640b5a..784f46fef1 100644 --- a/packages/vitnode/src/api/lib/route.ts +++ b/packages/vitnode/src/api/lib/route.ts @@ -57,25 +57,33 @@ export const findAdminRoutesWithoutStaffPermission = (hono: { .sort(); }; +export type BuildRouteConfig

= Omit & { + path: P; + withCaptcha?: boolean; +}; + +export interface BuildRouteOptions< + P extends string, + R extends BuildRouteConfig

, +> { + adminStaffPermission?: AdminStaffPermission; + handler: RouteHandler; + hook?: RouteValidationHook; + route: R; +} + export const buildRoute = < Plugin extends string, P extends string, - R extends Omit & { - path: P; - withCaptcha?: boolean; - }, + R extends BuildRouteConfig

, >({ route, handler, hook, pluginId, adminStaffPermission, -}: { - adminStaffPermission?: AdminStaffPermission; - handler: RouteHandler; - hook?: RouteValidationHook; +}: BuildRouteOptions & { pluginId: Plugin; - route: R; }): Route => { const tags = [pluginTag(pluginId), ...(route.tags ?? [])]; diff --git a/packages/vitnode/src/api/middlewares/global.middleware.ts b/packages/vitnode/src/api/middlewares/global.middleware.ts index 95a3c372da..6bed7b1f9f 100644 --- a/packages/vitnode/src/api/middlewares/global.middleware.ts +++ b/packages/vitnode/src/api/middlewares/global.middleware.ts @@ -50,6 +50,7 @@ import type { AiActionRegistry } from "../lib/ai/registry"; import type { BuildCronReturn } from "../lib/cron"; import type { RegisteredEditablePage } from "../lib/editable-pages"; import type { EventListenerConfig } from "../lib/events"; +import type { InstalledPluginRegistry } from "../lib/installed-plugins"; import type { ModerationRegistry } from "../lib/moderation/registry"; import type { NotificationWorkerSettings } from "../lib/notifications/preferences"; import type { NotificationRegistry } from "../lib/notifications/registry"; @@ -73,6 +74,7 @@ import { registerEditablePage, validateEditablePages, } from "../lib/editable-pages"; +import { buildInstalledPluginRegistry } from "../lib/installed-plugins"; import { loggerMiddleware, type LoggerMiddlewareType, @@ -162,6 +164,7 @@ export interface EnvVariablesVitNode { locales: LocaleConfig[]; sources: MessagesSource[]; }; + installedPlugins: InstalledPluginRegistry; metadata: { shortTitle?: string; title: string; @@ -228,6 +231,7 @@ export const globalMiddleware = ({ captcha, cron, events, + frontendPlugins, plugins, i18n, notifications, @@ -245,6 +249,7 @@ export const globalMiddleware = ({ | "dbProvider" | "email" | "events" + | "frontendPlugins" | "i18n" | "notifications" | "plugins" @@ -259,6 +264,10 @@ export const globalMiddleware = ({ const pluginsMetadata = plugins.map(plugin => ({ id: plugin.pluginId, })); + const installedPlugins = buildInstalledPluginRegistry({ + api: plugins, + frontend: frontendPlugins, + }); // Resolved once at boot: the packages installed here can't change per // request. With no `i18n` block the locale list is whatever the installed @@ -523,6 +532,7 @@ export const globalMiddleware = ({ cronSecret: CONFIG.cronJobSecret, hasCronAdapter: !!cron, plugins: pluginsMetadata, + installedPlugins, cron: cronMetadata, editablePages: editablePagesMetadata, queue: queueMetadata, diff --git a/packages/vitnode/src/api/modules/admin/admin.module.ts b/packages/vitnode/src/api/modules/admin/admin.module.ts index b89d203a6c..da59e6986d 100644 --- a/packages/vitnode/src/api/modules/admin/admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/admin.module.ts @@ -1,5 +1,4 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { advancedAdminModule } from "./advanced/advanced.admin.module"; import { aiAdminModule } from "./ai/ai.admin.module"; @@ -8,6 +7,7 @@ import { debugAdminModule } from "./debug/debug.admin.module"; import { filesAdminModule } from "./files/files.admin.module"; import { navigationAdminModule } from "./navigation/navigation.admin.module"; import { notificationsAdminModule } from "./notifications/notifications.admin.module"; +import { pluginsAdminModule } from "./plugins/plugins.admin.module"; import { rolesAdminModule } from "./roles/roles.admin.module"; import { sendNotificationRoute } from "./routes/notifications.route"; import { sessionAdminRoute } from "./routes/session.route"; @@ -15,7 +15,6 @@ import { staffAdminModule } from "./staff/staff.admin.module"; import { usersAdminModule } from "./users/users.admin.module"; export const adminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "admin", routes: [sessionAdminRoute, sendNotificationRoute], modules: [ @@ -29,6 +28,7 @@ export const adminModule = buildModule({ dashboardAdminModule, navigationAdminModule, notificationsAdminModule, + pluginsAdminModule, ], cronJobs: [], }); diff --git a/packages/vitnode/src/api/modules/admin/advanced/advanced.admin.module.ts b/packages/vitnode/src/api/modules/admin/advanced/advanced.admin.module.ts index 281b679804..7f68dd68e0 100644 --- a/packages/vitnode/src/api/modules/admin/advanced/advanced.admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/advanced/advanced.admin.module.ts @@ -1,11 +1,9 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { cronAdminModule } from "./cron/cron.admin.module"; import { queueAdminModule } from "./queue/queue.admin.module"; export const advancedAdminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "advanced", routes: [], modules: [cronAdminModule, queueAdminModule], diff --git a/packages/vitnode/src/api/modules/admin/advanced/cron/cron.admin.module.ts b/packages/vitnode/src/api/modules/admin/advanced/cron/cron.admin.module.ts index ef109d03ff..9b77c9c5eb 100644 --- a/packages/vitnode/src/api/modules/admin/advanced/cron/cron.admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/advanced/cron/cron.admin.module.ts @@ -1,12 +1,10 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { getCronsRoute } from "./routes/get.route"; import { getCronHealthRoute } from "./routes/health.route"; import { runCronRoute } from "./routes/run.route"; export const cronAdminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "cron", routes: [getCronsRoute, getCronHealthRoute, runCronRoute], }); diff --git a/packages/vitnode/src/api/modules/admin/advanced/cron/routes/get.route.ts b/packages/vitnode/src/api/modules/admin/advanced/cron/routes/get.route.ts index 7dfa50ef18..a71accac40 100644 --- a/packages/vitnode/src/api/modules/admin/advanced/cron/routes/get.route.ts +++ b/packages/vitnode/src/api/modules/admin/advanced/cron/routes/get.route.ts @@ -1,18 +1,16 @@ import { getColumns } from "drizzle-orm"; import z from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { withPagination, zodPaginationPageInfo, zodPaginationQuery, } from "@/api/lib/with-pagination"; -import { CONFIG_PLUGIN } from "@/config"; import { core_cron } from "@/database/cron"; import { isCronJobOverdue } from "@/lib/api/cron-health"; export const getCronsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "cron", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/advanced/cron/routes/health.route.ts b/packages/vitnode/src/api/modules/admin/advanced/cron/routes/health.route.ts index 42882d2189..f69636dde4 100644 --- a/packages/vitnode/src/api/modules/admin/advanced/cron/routes/health.route.ts +++ b/packages/vitnode/src/api/modules/admin/advanced/cron/routes/health.route.ts @@ -1,12 +1,10 @@ import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { loadCronHealth } from "@/api/modules/cron/helpers/load-cron-health"; -import { CONFIG_PLUGIN } from "@/config"; import { isCronSecretRejected } from "@/lib/config"; export const getCronHealthRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "cron", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/advanced/cron/routes/run.route.ts b/packages/vitnode/src/api/modules/admin/advanced/cron/routes/run.route.ts index 0433bb834e..aaf5f40f74 100644 --- a/packages/vitnode/src/api/modules/admin/advanced/cron/routes/run.route.ts +++ b/packages/vitnode/src/api/modules/admin/advanced/cron/routes/run.route.ts @@ -3,13 +3,11 @@ import { z } from "zod"; import type { CronJobConfig } from "@/api/lib/cron"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { core_cron } from "@/database/cron"; import { getNextCronRunDate } from "@/lib/api/get-next-cron-run-date"; export const runCronRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "cron", permission: "can_run" }, route: { method: "post", diff --git a/packages/vitnode/src/api/modules/admin/advanced/queue/queue.admin.module.ts b/packages/vitnode/src/api/modules/admin/advanced/queue/queue.admin.module.ts index 3a09e76924..345b80229e 100644 --- a/packages/vitnode/src/api/modules/admin/advanced/queue/queue.admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/advanced/queue/queue.admin.module.ts @@ -1,10 +1,8 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { getQueueTasksRoute } from "./routes/get.route"; export const queueAdminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "queue", routes: [getQueueTasksRoute], }); diff --git a/packages/vitnode/src/api/modules/admin/advanced/queue/routes/get.route.ts b/packages/vitnode/src/api/modules/admin/advanced/queue/routes/get.route.ts index 3bdfce8050..ea41a0b75e 100644 --- a/packages/vitnode/src/api/modules/admin/advanced/queue/routes/get.route.ts +++ b/packages/vitnode/src/api/modules/admin/advanced/queue/routes/get.route.ts @@ -1,13 +1,12 @@ import { inArray } from "drizzle-orm"; import z from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { withPagination, zodPaginationPageInfo, zodPaginationQuery, } from "@/api/lib/with-pagination"; -import { CONFIG_PLUGIN } from "@/config"; import { core_queue } from "@/database/queue"; const QUEUE_STATUSES = [ @@ -18,7 +17,6 @@ const QUEUE_STATUSES = [ ] as const; export const getQueueTasksRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "queue", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/ai/ai.admin.module.ts b/packages/vitnode/src/api/modules/admin/ai/ai.admin.module.ts index 926773cc48..4c853b4468 100644 --- a/packages/vitnode/src/api/modules/admin/ai/ai.admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/ai/ai.admin.module.ts @@ -1,5 +1,4 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { deleteAiUserOverrideAdminRoute, @@ -31,7 +30,6 @@ import { updateAiActionAdminRoute } from "./routes/update-action.route"; import { updateAiSettingsAdminRoute } from "./routes/update-settings.route"; export const aiAdminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "ai", routes: [ aiOverviewAdminRoute, diff --git a/packages/vitnode/src/api/modules/admin/ai/routes/access.route.ts b/packages/vitnode/src/api/modules/admin/ai/routes/access.route.ts index 82e409037e..53892cc97d 100644 --- a/packages/vitnode/src/api/modules/admin/ai/routes/access.route.ts +++ b/packages/vitnode/src/api/modules/admin/ai/routes/access.route.ts @@ -1,9 +1,8 @@ import { and, eq, inArray } from "drizzle-orm"; import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { formatDecimal, parseDecimal } from "@/api/lib/ai/decimal"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { core_ai_role_permissions, core_ai_role_policies, @@ -19,7 +18,6 @@ const decimalOrNull = (value: null | string | undefined) => : formatDecimal(parseDecimal(value)); export const getAiRoleAccessAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "ai", permission: "can_view" }, route: { method: "get", @@ -153,7 +151,6 @@ export const getAiRoleAccessAdminRoute = buildRoute({ }); export const getAiUserOverrideAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "ai", permission: "can_view" }, route: { method: "get", @@ -203,7 +200,6 @@ export const getAiUserOverrideAdminRoute = buildRoute({ }); export const updateAiRoleAccessAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "ai", permission: "can_manage" }, route: { method: "put", @@ -289,7 +285,6 @@ export const updateAiRoleAccessAdminRoute = buildRoute({ }); export const updateAiUserOverrideAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "ai", permission: "can_manage" }, route: { method: "put", @@ -336,7 +331,6 @@ export const updateAiUserOverrideAdminRoute = buildRoute({ }); export const deleteAiUserOverrideAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "ai", permission: "can_manage" }, route: { method: "delete", diff --git a/packages/vitnode/src/api/modules/admin/ai/routes/actions.route.ts b/packages/vitnode/src/api/modules/admin/ai/routes/actions.route.ts index 23df964d44..58180ee0ca 100644 --- a/packages/vitnode/src/api/modules/admin/ai/routes/actions.route.ts +++ b/packages/vitnode/src/api/modules/admin/ai/routes/actions.route.ts @@ -1,18 +1,16 @@ import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { DEFAULT_AI_MODEL_CAPABILITIES, missingCapabilities, } from "@/api/lib/ai/capabilities"; import { projectAiAction } from "@/api/lib/ai/registry"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { core_ai_action_settings } from "@/database/ai"; import { zodAiAction } from "../schemas"; export const listAiActionsAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "ai", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/ai/routes/assist-stream.route.ts b/packages/vitnode/src/api/modules/admin/ai/routes/assist-stream.route.ts index 3e042b2113..5d921175ba 100644 --- a/packages/vitnode/src/api/modules/admin/ai/routes/assist-stream.route.ts +++ b/packages/vitnode/src/api/modules/admin/ai/routes/assist-stream.route.ts @@ -1,15 +1,13 @@ import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { zodAiRunBody } from "@/api/lib/ai/run-schemas"; import { AI_NDJSON_HEADERS, aiNdjsonStream, } from "@/api/lib/ai/stream-response"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; export const assistStreamAiAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: "Stream a registered AI text action for the signed-in admin.", @@ -43,7 +41,6 @@ export const assistStreamAiAdminRoute = buildRoute({ }); export const estimateAiAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: diff --git a/packages/vitnode/src/api/modules/admin/ai/routes/assist.route.ts b/packages/vitnode/src/api/modules/admin/ai/routes/assist.route.ts index 434244b09f..107a62d8c4 100644 --- a/packages/vitnode/src/api/modules/admin/ai/routes/assist.route.ts +++ b/packages/vitnode/src/api/modules/admin/ai/routes/assist.route.ts @@ -1,13 +1,11 @@ +import { buildRoute } from "@/api/builders"; import { zodAiErrorBody, zodAiRunBody, zodAiRunResponse, } from "@/api/lib/ai/run-schemas"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; export const assistAiAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: "Run a registered AI action for the signed-in admin.", diff --git a/packages/vitnode/src/api/modules/admin/ai/routes/available.route.ts b/packages/vitnode/src/api/modules/admin/ai/routes/available.route.ts index 101fe84785..c284ce41ec 100644 --- a/packages/vitnode/src/api/modules/admin/ai/routes/available.route.ts +++ b/packages/vitnode/src/api/modules/admin/ai/routes/available.route.ts @@ -1,12 +1,10 @@ import { HTTPException } from "hono/http-exception"; import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { availableAiActions } from "@/api/lib/ai/available-actions"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; export const availableAiActionsAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: "The AI actions you may start in the AdminCP.", diff --git a/packages/vitnode/src/api/modules/admin/ai/routes/feedback.route.ts b/packages/vitnode/src/api/modules/admin/ai/routes/feedback.route.ts index f4abd26d92..88d4f194fb 100644 --- a/packages/vitnode/src/api/modules/admin/ai/routes/feedback.route.ts +++ b/packages/vitnode/src/api/modules/admin/ai/routes/feedback.route.ts @@ -2,12 +2,10 @@ import { and, eq } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { core_ai_runs } from "@/database/ai"; export const aiFeedbackAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: "Record whether you accepted an AI suggestion.", diff --git a/packages/vitnode/src/api/modules/admin/ai/routes/history.route.ts b/packages/vitnode/src/api/modules/admin/ai/routes/history.route.ts index ad8e3989b4..889e517936 100644 --- a/packages/vitnode/src/api/modules/admin/ai/routes/history.route.ts +++ b/packages/vitnode/src/api/modules/admin/ai/routes/history.route.ts @@ -2,13 +2,12 @@ import { and, asc, eq, gte, lt } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { withPagination, zodPaginationPageInfo, zodPaginationQuery, } from "@/api/lib/with-pagination"; -import { CONFIG_PLUGIN } from "@/config"; import { core_ai_calls, core_ai_cost_adjustments, @@ -60,7 +59,6 @@ const toRow = ({ }); export const listAiHistoryAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "ai", permission: "can_view" }, route: { method: "get", @@ -129,7 +127,6 @@ export const listAiHistoryAdminRoute = buildRoute({ }); export const showAiRunAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "ai", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/ai/routes/models.route.ts b/packages/vitnode/src/api/modules/admin/ai/routes/models.route.ts index 74b2e8a056..36c79c18fc 100644 --- a/packages/vitnode/src/api/modules/admin/ai/routes/models.route.ts +++ b/packages/vitnode/src/api/modules/admin/ai/routes/models.route.ts @@ -1,14 +1,12 @@ import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { DEFAULT_AI_MODEL_CAPABILITIES } from "@/api/lib/ai/capabilities"; import { providerIdOf, providerModelIdOf } from "@/api/lib/ai/runner"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { zodAiModel } from "../schemas"; export const listAiModelsAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "ai", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/ai/routes/overview.route.ts b/packages/vitnode/src/api/modules/admin/ai/routes/overview.route.ts index 6d6ba02018..9a16791c55 100644 --- a/packages/vitnode/src/api/modules/admin/ai/routes/overview.route.ts +++ b/packages/vitnode/src/api/modules/admin/ai/routes/overview.route.ts @@ -1,9 +1,8 @@ import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { loadAiOverview } from "@/api/lib/ai/admin-stats"; import { localDayOf } from "@/api/lib/ai/periods"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { AI_OVERVIEW_PRESETS, aiMonthOf, @@ -70,7 +69,6 @@ export const zodAiOverview = z.object({ }); export const aiOverviewAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "ai", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/ai/routes/settings.route.ts b/packages/vitnode/src/api/modules/admin/ai/routes/settings.route.ts index f3e1e15bba..ff2f98a4ce 100644 --- a/packages/vitnode/src/api/modules/admin/ai/routes/settings.route.ts +++ b/packages/vitnode/src/api/modules/admin/ai/routes/settings.route.ts @@ -1,15 +1,13 @@ +import { buildRoute } from "@/api/builders"; import { formatDecimal, formatDecimalOrNull } from "@/api/lib/ai/decimal"; import { AI_POINTS_CONVERSION, AI_POINTS_CONVERSION_VERSION, } from "@/api/lib/ai/ledger"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { zodAiSettingsResponse } from "../schemas"; export const getAiSettingsAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "ai", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/ai/routes/translation-sources.route.ts b/packages/vitnode/src/api/modules/admin/ai/routes/translation-sources.route.ts index 9560e8d501..542597e723 100644 --- a/packages/vitnode/src/api/modules/admin/ai/routes/translation-sources.route.ts +++ b/packages/vitnode/src/api/modules/admin/ai/routes/translation-sources.route.ts @@ -6,9 +6,8 @@ import { z } from "zod"; import type { ContentId } from "@/content/ids"; +import { buildRoute } from "@/api/builders"; import { checkStaffPermission } from "@/api/lib/check-staff-permission"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { contentAnyIdSchema, contentIdKey, @@ -57,7 +56,6 @@ const zodTranslationSourceRecord = z.object({ }); export const getTranslationSourcesAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: @@ -112,7 +110,6 @@ export const getTranslationSourcesAdminRoute = buildRoute({ }); export const saveTranslationSourcesAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "put", description: diff --git a/packages/vitnode/src/api/modules/admin/ai/routes/update-action.route.ts b/packages/vitnode/src/api/modules/admin/ai/routes/update-action.route.ts index b68297cabf..d3c38ad701 100644 --- a/packages/vitnode/src/api/modules/admin/ai/routes/update-action.route.ts +++ b/packages/vitnode/src/api/modules/admin/ai/routes/update-action.route.ts @@ -1,18 +1,16 @@ import { HTTPException } from "hono/http-exception"; import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { DEFAULT_AI_MODEL_CAPABILITIES, missingCapabilities, } from "@/api/lib/ai/capabilities"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { core_ai_action_settings } from "@/database/ai"; import { zodAiActionSettingsInput } from "../schemas"; export const updateAiActionAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "ai", permission: "can_manage" }, route: { method: "put", diff --git a/packages/vitnode/src/api/modules/admin/ai/routes/update-settings.route.ts b/packages/vitnode/src/api/modules/admin/ai/routes/update-settings.route.ts index 762b2b0a62..d872929b96 100644 --- a/packages/vitnode/src/api/modules/admin/ai/routes/update-settings.route.ts +++ b/packages/vitnode/src/api/modules/admin/ai/routes/update-settings.route.ts @@ -1,14 +1,12 @@ import { HTTPException } from "hono/http-exception"; import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { saveAiSettings } from "@/api/lib/ai/settings-store"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { zodAiSettings } from "../schemas"; export const updateAiSettingsAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "ai", permission: "can_manage" }, route: { method: "put", diff --git a/packages/vitnode/src/api/modules/admin/dashboard/dashboard.admin.module.ts b/packages/vitnode/src/api/modules/admin/dashboard/dashboard.admin.module.ts index 5d3303a7bd..eff48d547a 100644 --- a/packages/vitnode/src/api/modules/admin/dashboard/dashboard.admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/dashboard/dashboard.admin.module.ts @@ -1,12 +1,10 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { getDashboardLayoutAdminRoute } from "./routes/get-layout.route"; import { saveDashboardLayoutAdminRoute } from "./routes/save-layout.route"; import { saveDashboardWidgetSettingsAdminRoute } from "./routes/save-widget-settings.route"; export const dashboardAdminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "dashboard", routes: [ getDashboardLayoutAdminRoute, diff --git a/packages/vitnode/src/api/modules/admin/dashboard/routes/get-layout.route.ts b/packages/vitnode/src/api/modules/admin/dashboard/routes/get-layout.route.ts index 09a213ad6c..086c40eebd 100644 --- a/packages/vitnode/src/api/modules/admin/dashboard/routes/get-layout.route.ts +++ b/packages/vitnode/src/api/modules/admin/dashboard/routes/get-layout.route.ts @@ -1,13 +1,11 @@ import { z } from "@hono/zod-openapi"; import { HTTPException } from "hono/http-exception"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { getDashboardWidgets, zodStoredDashboardWidget } from "../lib/layout"; export const getDashboardLayoutAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "dashboard", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/dashboard/routes/save-layout.route.ts b/packages/vitnode/src/api/modules/admin/dashboard/routes/save-layout.route.ts index 5af03feb82..f35c34063a 100644 --- a/packages/vitnode/src/api/modules/admin/dashboard/routes/save-layout.route.ts +++ b/packages/vitnode/src/api/modules/admin/dashboard/routes/save-layout.route.ts @@ -1,8 +1,7 @@ import { z } from "@hono/zod-openapi"; import { HTTPException } from "hono/http-exception"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { mergeLayoutForSave, @@ -13,7 +12,6 @@ import { export const zodSaveDashboardLayoutSchema = zodDashboardLayout; export const saveDashboardLayoutAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "dashboard", permission: "can_edit" }, route: { method: "put", diff --git a/packages/vitnode/src/api/modules/admin/dashboard/routes/save-widget-settings.route.ts b/packages/vitnode/src/api/modules/admin/dashboard/routes/save-widget-settings.route.ts index 3c54bc7ec9..555eb62b19 100644 --- a/packages/vitnode/src/api/modules/admin/dashboard/routes/save-widget-settings.route.ts +++ b/packages/vitnode/src/api/modules/admin/dashboard/routes/save-widget-settings.route.ts @@ -1,8 +1,7 @@ import { z } from "@hono/zod-openapi"; import { HTTPException } from "hono/http-exception"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { isSettingsTooLarge, @@ -19,7 +18,6 @@ export const zodSaveWidgetSettingsSchema = z.object({ }); export const saveDashboardWidgetSettingsAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "dashboard", permission: "can_edit" }, route: { method: "put", diff --git a/packages/vitnode/src/api/modules/admin/debug/debug.admin.module.ts b/packages/vitnode/src/api/modules/admin/debug/debug.admin.module.ts index dd9af296c3..2f5b99bb1f 100644 --- a/packages/vitnode/src/api/modules/admin/debug/debug.admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/debug/debug.admin.module.ts @@ -1,5 +1,5 @@ -import { CONFIG_PLUGIN } from "../../../../config"; -import { buildModule } from "../../../lib/module"; +import { buildModule } from "@/api/builders"; + import { clearSearchDebugAdminRoute } from "./routes/clear-search.route"; import { contentStatusDebugAdminRoute } from "./routes/content-status.route"; import { integrationsDebugAdminRoute } from "./routes/integrations.route"; @@ -12,7 +12,6 @@ import { testAiDebugAdminRoute } from "./routes/test-ai.route"; import { testStorageUploadDebugAdminRoute } from "./routes/test-storage-upload.route"; export const debugAdminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "debug", routes: [ logsDebugAdminRoute, diff --git a/packages/vitnode/src/api/modules/admin/debug/routes/clear-search.route.ts b/packages/vitnode/src/api/modules/admin/debug/routes/clear-search.route.ts index cfb1e5d62e..47ea1e11ae 100644 --- a/packages/vitnode/src/api/modules/admin/debug/routes/clear-search.route.ts +++ b/packages/vitnode/src/api/modules/admin/debug/routes/clear-search.route.ts @@ -1,15 +1,13 @@ import { HTTPException } from "hono/http-exception"; import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; export const zodClearSearchSchema = z.object({ itemType: z.string().min(1), }); export const clearSearchDebugAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "system", permission: "can_manage_search" }, route: { method: "post", diff --git a/packages/vitnode/src/api/modules/admin/debug/routes/content-status.route.ts b/packages/vitnode/src/api/modules/admin/debug/routes/content-status.route.ts index aa326ce5ca..c14f430818 100644 --- a/packages/vitnode/src/api/modules/admin/debug/routes/content-status.route.ts +++ b/packages/vitnode/src/api/modules/admin/debug/routes/content-status.route.ts @@ -1,7 +1,6 @@ import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { contentEngineDiagnostics } from "@/content/server/diagnostics"; const localeDriftSchema = z.object({ @@ -64,7 +63,6 @@ const contentTypeSchema = z.object({ }); export const contentStatusDebugAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "system", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/debug/routes/integrations.route.ts b/packages/vitnode/src/api/modules/admin/debug/routes/integrations.route.ts index b0e3a42b4c..bbb4361db5 100644 --- a/packages/vitnode/src/api/modules/admin/debug/routes/integrations.route.ts +++ b/packages/vitnode/src/api/modules/admin/debug/routes/integrations.route.ts @@ -1,17 +1,14 @@ import { inArray, sql } from "drizzle-orm"; import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { loadCronHealth } from "@/api/modules/cron/helpers/load-cron-health"; -import { CONFIG_PLUGIN } from "@/config"; import { core_queue } from "@/database/queue"; import { getQueueStatus } from "@/lib/api/get-queue-status"; import { INSECURE_DEFAULT_CRON_SECRET } from "@/lib/config"; import { isRealtimePubSubEnabled, isWebSocketEnabled } from "@/ws/registry"; -import { buildRoute } from "../../../../lib/route"; - export const integrationsDebugAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "system", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/debug/routes/logs.route.ts b/packages/vitnode/src/api/modules/admin/debug/routes/logs.route.ts index e4283806c5..19a2df46c8 100644 --- a/packages/vitnode/src/api/modules/admin/debug/routes/logs.route.ts +++ b/packages/vitnode/src/api/modules/admin/debug/routes/logs.route.ts @@ -1,11 +1,10 @@ import { eq } from "drizzle-orm"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { core_logs } from "@/database/logs"; import { core_users } from "../../../../../database/users"; -import { buildRoute } from "../../../../lib/route"; import { withPagination, zodPaginationPageInfo, @@ -13,7 +12,6 @@ import { } from "../../../../lib/with-pagination"; export const logsDebugAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "debug", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/debug/routes/queue.route.ts b/packages/vitnode/src/api/modules/admin/debug/routes/queue.route.ts index c6a30a18e1..96e6b1dbbe 100644 --- a/packages/vitnode/src/api/modules/admin/debug/routes/queue.route.ts +++ b/packages/vitnode/src/api/modules/admin/debug/routes/queue.route.ts @@ -1,8 +1,7 @@ import { asc, desc, inArray, sql } from "drizzle-orm"; import z from "zod"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { core_queue } from "@/database/queue"; const QUEUE_STATUSES = [ @@ -13,7 +12,6 @@ const QUEUE_STATUSES = [ ] as const; export const queueDebugAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "debug", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/debug/routes/rebuild-search.route.ts b/packages/vitnode/src/api/modules/admin/debug/routes/rebuild-search.route.ts index c2d26e6b0e..d35e0b7777 100644 --- a/packages/vitnode/src/api/modules/admin/debug/routes/rebuild-search.route.ts +++ b/packages/vitnode/src/api/modules/admin/debug/routes/rebuild-search.route.ts @@ -1,8 +1,7 @@ import { HTTPException } from "hono/http-exception"; import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; export const zodRebuildSearchSchema = z.object({ // Rebuild a single collection; omit to rebuild the whole index. @@ -10,7 +9,6 @@ export const zodRebuildSearchSchema = z.object({ }); export const rebuildSearchDebugAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "system", permission: "can_manage_search" }, route: { method: "post", diff --git a/packages/vitnode/src/api/modules/admin/debug/routes/search-status.route.ts b/packages/vitnode/src/api/modules/admin/debug/routes/search-status.route.ts index 8137d5f4bb..ffb397a87e 100644 --- a/packages/vitnode/src/api/modules/admin/debug/routes/search-status.route.ts +++ b/packages/vitnode/src/api/modules/admin/debug/routes/search-status.route.ts @@ -10,9 +10,8 @@ import { } from "drizzle-orm"; import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { searchDocumentOwner } from "@/api/models/search"; -import { CONFIG_PLUGIN } from "@/config"; import { core_logs } from "@/database/logs"; import { core_search_index } from "@/database/search"; @@ -48,7 +47,6 @@ const syncErrorSchema = z.object({ }); export const searchStatusDebugAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "system", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/debug/routes/send-test-email.route.ts b/packages/vitnode/src/api/modules/admin/debug/routes/send-test-email.route.ts index 7adc8e8347..de1d27b422 100644 --- a/packages/vitnode/src/api/modules/admin/debug/routes/send-test-email.route.ts +++ b/packages/vitnode/src/api/modules/admin/debug/routes/send-test-email.route.ts @@ -1,8 +1,7 @@ import { HTTPException } from "hono/http-exception"; import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import TestEmailTemplate from "@/emails/test-email"; export const zodSendTestEmailSchema = z.object({ @@ -12,7 +11,6 @@ export const zodSendTestEmailSchema = z.object({ }); export const sendTestEmailDebugAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "system", permission: "can_send_test_email" }, route: { method: "post", diff --git a/packages/vitnode/src/api/modules/admin/debug/routes/test-ai.route.ts b/packages/vitnode/src/api/modules/admin/debug/routes/test-ai.route.ts index b34cb616cf..6f1ee29635 100644 --- a/packages/vitnode/src/api/modules/admin/debug/routes/test-ai.route.ts +++ b/packages/vitnode/src/api/modules/admin/debug/routes/test-ai.route.ts @@ -2,8 +2,7 @@ import { streamText } from "ai"; import { HTTPException } from "hono/http-exception"; import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; export const zodTestAiSchema = z.object({ // A configured model id (see `ai.models`). Omit for the default (first) model. @@ -12,7 +11,6 @@ export const zodTestAiSchema = z.object({ }); export const testAiDebugAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "system", permission: "can_test_ai" }, route: { method: "post", diff --git a/packages/vitnode/src/api/modules/admin/debug/routes/test-storage-upload.route.ts b/packages/vitnode/src/api/modules/admin/debug/routes/test-storage-upload.route.ts index 889d7204a2..b384264d31 100644 --- a/packages/vitnode/src/api/modules/admin/debug/routes/test-storage-upload.route.ts +++ b/packages/vitnode/src/api/modules/admin/debug/routes/test-storage-upload.route.ts @@ -1,8 +1,7 @@ import { HTTPException } from "hono/http-exception"; import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; const ADMIN_TEST_MAX_BYTES = 10 * 1024 * 1024; // 10 MB @@ -15,7 +14,6 @@ const ADMIN_TEST_MIME_TYPES = [ ]; export const testStorageUploadDebugAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "system", permission: "can_test_storage" }, route: { method: "post", diff --git a/packages/vitnode/src/api/modules/admin/files/files.admin.module.ts b/packages/vitnode/src/api/modules/admin/files/files.admin.module.ts index 829081edb3..1d8f58e7eb 100644 --- a/packages/vitnode/src/api/modules/admin/files/files.admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/files/files.admin.module.ts @@ -1,5 +1,4 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { deleteFileAltAdminRoute, @@ -12,7 +11,6 @@ import { downloadFileAdminRoute } from "./routes/download.route"; import { listFilesAdminRoute } from "./routes/list.route"; export const filesAdminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "files", routes: [ listFilesAdminRoute, diff --git a/packages/vitnode/src/api/modules/admin/files/routes/alt.route.ts b/packages/vitnode/src/api/modules/admin/files/routes/alt.route.ts index 33f439da72..171e0a5caf 100644 --- a/packages/vitnode/src/api/modules/admin/files/routes/alt.route.ts +++ b/packages/vitnode/src/api/modules/admin/files/routes/alt.route.ts @@ -2,15 +2,13 @@ import { and, asc, eq } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { core_files, core_files_alt } from "@/database/files"; import { core_languages } from "@/database/languages"; const zodAltPolicy = z.enum(["automatic", "manual", "disabled"]); export const getFileAltAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "files", permission: "can_view" }, route: { method: "get", @@ -86,7 +84,6 @@ export const getFileAltAdminRoute = buildRoute({ }); export const updateFileAltAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "files", permission: "can_edit_alt" }, route: { method: "put", @@ -160,7 +157,6 @@ export const updateFileAltAdminRoute = buildRoute({ }); export const deleteFileAltAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "files", permission: "can_edit_alt" }, route: { method: "delete", @@ -205,7 +201,6 @@ export const deleteFileAltAdminRoute = buildRoute({ }); export const updateFileAltPolicyAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "files", permission: "can_edit_alt" }, route: { method: "put", diff --git a/packages/vitnode/src/api/modules/admin/files/routes/delete.route.ts b/packages/vitnode/src/api/modules/admin/files/routes/delete.route.ts index f1960dcc25..caa1344715 100644 --- a/packages/vitnode/src/api/modules/admin/files/routes/delete.route.ts +++ b/packages/vitnode/src/api/modules/admin/files/routes/delete.route.ts @@ -1,10 +1,8 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; export const deleteFileAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "files", permission: "can_delete" }, route: { method: "delete", diff --git a/packages/vitnode/src/api/modules/admin/files/routes/download.route.ts b/packages/vitnode/src/api/modules/admin/files/routes/download.route.ts index 2840965ca3..b05dd3c2d2 100644 --- a/packages/vitnode/src/api/modules/admin/files/routes/download.route.ts +++ b/packages/vitnode/src/api/modules/admin/files/routes/download.route.ts @@ -1,13 +1,11 @@ import { z } from "@hono/zod-openapi"; import { eq } from "drizzle-orm"; +import { buildRoute } from "@/api/builders"; import { attachStoredFile } from "@/api/lib/file-download"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { core_files } from "@/database/files"; export const downloadFileAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "files", permission: "can_download" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/files/routes/list.route.ts b/packages/vitnode/src/api/modules/admin/files/routes/list.route.ts index c5e3424f28..7842867228 100644 --- a/packages/vitnode/src/api/modules/admin/files/routes/list.route.ts +++ b/packages/vitnode/src/api/modules/admin/files/routes/list.route.ts @@ -1,21 +1,19 @@ import { eq } from "drizzle-orm"; import z from "zod"; +import { buildRoute } from "@/api/builders"; import { resolveRoleNames } from "@/api/lib/resolve-role-names"; -import { buildRoute } from "@/api/lib/route"; import { withPagination, zodPaginationPageInfo, zodPaginationQuery, } from "@/api/lib/with-pagination"; -import { CONFIG_PLUGIN } from "@/config"; import { core_files } from "@/database/files"; import { core_roles } from "@/database/roles"; import { core_users } from "@/database/users"; import { parseImageDimensions } from "@/lib/api/upload"; export const listFilesAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "files", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/navigation/lib/read-navigation.ts b/packages/vitnode/src/api/modules/admin/navigation/lib/read-navigation.ts index 4fa5bd3878..f5c3b3330f 100644 --- a/packages/vitnode/src/api/modules/admin/navigation/lib/read-navigation.ts +++ b/packages/vitnode/src/api/modules/admin/navigation/lib/read-navigation.ts @@ -12,7 +12,6 @@ import type { } from "@/lib/navigation"; import { findNavigationPreset } from "@/api/lib/navigation-presets"; -import { CONFIG_PLUGIN } from "@/config"; import { core_languages_words } from "@/database/languages"; import { core_navigation } from "@/database/navigation"; import { @@ -21,6 +20,7 @@ import { NAVIGATION_TABLE_NAME, NAVIGATION_WORDS, } from "@/lib/navigation"; +import { manifest } from "@/manifest"; import { navigationLocationOf } from "./location"; @@ -95,7 +95,7 @@ export const readNavigationWords = async ( .from(core_languages_words) .where( and( - eq(core_languages_words.pluginCode, CONFIG_PLUGIN.pluginId), + eq(core_languages_words.pluginCode, manifest.id), eq(core_languages_words.tableName, NAVIGATION_TABLE_NAME), inArray(core_languages_words.variable, [ NAVIGATION_WORDS.title, diff --git a/packages/vitnode/src/api/modules/admin/navigation/lib/words.ts b/packages/vitnode/src/api/modules/admin/navigation/lib/words.ts index ca42ec1a85..00b5835c7e 100644 --- a/packages/vitnode/src/api/modules/admin/navigation/lib/words.ts +++ b/packages/vitnode/src/api/modules/admin/navigation/lib/words.ts @@ -3,8 +3,8 @@ import type { Context } from "hono"; import type { NavigationText } from "@/lib/navigation"; import { saveLanguageWords } from "@/api/lib/save-language-words"; -import { CONFIG_PLUGIN } from "@/config"; import { NAVIGATION_TABLE_NAME, NAVIGATION_WORDS } from "@/lib/navigation"; +import { manifest } from "@/manifest"; const nonEmpty = (values: readonly NavigationText[]): NavigationText[] => values @@ -28,7 +28,7 @@ export const saveNavigationWords = async ( if (title !== undefined) { await saveLanguageWords(c, { itemId, - pluginCode: CONFIG_PLUGIN.pluginId, + pluginCode: manifest.id, tableName: NAVIGATION_TABLE_NAME, values: nonEmpty(title), variable: NAVIGATION_WORDS.title, @@ -38,7 +38,7 @@ export const saveNavigationWords = async ( if (description !== undefined) { await saveLanguageWords(c, { itemId, - pluginCode: CONFIG_PLUGIN.pluginId, + pluginCode: manifest.id, tableName: NAVIGATION_TABLE_NAME, values: nonEmpty(description), variable: NAVIGATION_WORDS.description, diff --git a/packages/vitnode/src/api/modules/admin/navigation/navigation.admin.module.ts b/packages/vitnode/src/api/modules/admin/navigation/navigation.admin.module.ts index 8d620c2bec..05ccfc5b5d 100644 --- a/packages/vitnode/src/api/modules/admin/navigation/navigation.admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/navigation/navigation.admin.module.ts @@ -1,5 +1,4 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { createNavigationAdminRoute } from "./routes/create.route"; import { deleteNavigationAdminRoute } from "./routes/delete.route"; @@ -9,7 +8,6 @@ import { reorderNavigationAdminRoute } from "./routes/reorder.route"; import { updateNavigationAdminRoute } from "./routes/update.route"; export const navigationAdminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "navigation", routes: [ listNavigationAdminRoute, diff --git a/packages/vitnode/src/api/modules/admin/navigation/routes/create.route.ts b/packages/vitnode/src/api/modules/admin/navigation/routes/create.route.ts index b9cb8ea291..ddc5c6892b 100644 --- a/packages/vitnode/src/api/modules/admin/navigation/routes/create.route.ts +++ b/packages/vitnode/src/api/modules/admin/navigation/routes/create.route.ts @@ -1,9 +1,8 @@ import { z } from "@hono/zod-openapi"; import { and, eq, sql } from "drizzle-orm"; +import { buildRoute } from "@/api/builders"; import { findNavigationPreset } from "@/api/lib/navigation-presets"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { core_navigation } from "@/database/navigation"; import { isValidNavigationHref } from "@/lib/navigation"; @@ -39,7 +38,6 @@ export const NAVIGATION_PARENT_ERRORS = { } as const; export const createNavigationAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "navigation", permission: "can_create" }, route: { method: "post", diff --git a/packages/vitnode/src/api/modules/admin/navigation/routes/delete.route.ts b/packages/vitnode/src/api/modules/admin/navigation/routes/delete.route.ts index b1a811e2e6..b37c65c293 100644 --- a/packages/vitnode/src/api/modules/admin/navigation/routes/delete.route.ts +++ b/packages/vitnode/src/api/modules/admin/navigation/routes/delete.route.ts @@ -1,16 +1,15 @@ import { z } from "@hono/zod-openapi"; import { and, asc, desc, eq, inArray, isNull } from "drizzle-orm"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { core_languages_words } from "@/database/languages"; import { core_navigation } from "@/database/navigation"; import { NAVIGATION_TABLE_NAME, NAVIGATION_WORDS } from "@/lib/navigation"; +import { manifest } from "@/manifest"; import { expireNavigationCache } from "../lib/cache"; export const deleteNavigationAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "navigation", permission: "can_delete" }, route: { method: "delete", @@ -91,7 +90,7 @@ export const deleteNavigationAdminRoute = buildRoute({ .delete(core_languages_words) .where( and( - eq(core_languages_words.pluginCode, CONFIG_PLUGIN.pluginId), + eq(core_languages_words.pluginCode, manifest.id), eq(core_languages_words.tableName, NAVIGATION_TABLE_NAME), inArray(core_languages_words.variable, [ NAVIGATION_WORDS.title, diff --git a/packages/vitnode/src/api/modules/admin/navigation/routes/list.route.ts b/packages/vitnode/src/api/modules/admin/navigation/routes/list.route.ts index 6eff44892e..924162284f 100644 --- a/packages/vitnode/src/api/modules/admin/navigation/routes/list.route.ts +++ b/packages/vitnode/src/api/modules/admin/navigation/routes/list.route.ts @@ -1,7 +1,6 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { readNavigationRecords, @@ -10,7 +9,6 @@ import { import { zodAdminNavigationItemSchema } from "../lib/schema"; export const listNavigationAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "navigation", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/navigation/routes/presets.route.ts b/packages/vitnode/src/api/modules/admin/navigation/routes/presets.route.ts index 8d6b0376b2..321c3b780e 100644 --- a/packages/vitnode/src/api/modules/admin/navigation/routes/presets.route.ts +++ b/packages/vitnode/src/api/modules/admin/navigation/routes/presets.route.ts @@ -1,12 +1,10 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { zodNavigationPresetSchema } from "../lib/schema"; export const presetsNavigationAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "navigation", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/navigation/routes/reorder.route.ts b/packages/vitnode/src/api/modules/admin/navigation/routes/reorder.route.ts index eff4ba1dd6..0b6491388b 100644 --- a/packages/vitnode/src/api/modules/admin/navigation/routes/reorder.route.ts +++ b/packages/vitnode/src/api/modules/admin/navigation/routes/reorder.route.ts @@ -1,8 +1,7 @@ import { z } from "@hono/zod-openapi"; import { eq } from "drizzle-orm"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { core_navigation } from "@/database/navigation"; import { expireNavigationCache } from "../lib/cache"; @@ -20,7 +19,6 @@ const REORDER_ERRORS = { } as const; export const reorderNavigationAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "navigation", permission: "can_edit" }, route: { method: "post", diff --git a/packages/vitnode/src/api/modules/admin/navigation/routes/update.route.ts b/packages/vitnode/src/api/modules/admin/navigation/routes/update.route.ts index 202b2c7680..73c1198590 100644 --- a/packages/vitnode/src/api/modules/admin/navigation/routes/update.route.ts +++ b/packages/vitnode/src/api/modules/admin/navigation/routes/update.route.ts @@ -1,8 +1,7 @@ import { z } from "@hono/zod-openapi"; import { eq } from "drizzle-orm"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { core_navigation } from "@/database/navigation"; import { isValidNavigationHref } from "@/lib/navigation"; @@ -27,7 +26,6 @@ import { const errorSchema = z.object({ error: z.string() }); export const updateNavigationAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "navigation", permission: "can_edit" }, route: { method: "patch", diff --git a/packages/vitnode/src/api/modules/admin/notifications/notifications.admin.module.ts b/packages/vitnode/src/api/modules/admin/notifications/notifications.admin.module.ts index 2ad9600bf7..97355ca54c 100644 --- a/packages/vitnode/src/api/modules/admin/notifications/notifications.admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/notifications/notifications.admin.module.ts @@ -1,5 +1,4 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { cancelQueuedNotificationEmailsRoute, @@ -22,7 +21,6 @@ import { updateNotificationTypePolicyRoute } from "./routes/settings.route"; import { getNotificationStatsRoute } from "./routes/stats.route"; export const notificationsAdminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "notifications", routes: [ getNotificationsOverviewRoute, diff --git a/packages/vitnode/src/api/modules/admin/notifications/routes/danger.route.ts b/packages/vitnode/src/api/modules/admin/notifications/routes/danger.route.ts index bc3c593d10..f0f1d3383f 100644 --- a/packages/vitnode/src/api/modules/admin/notifications/routes/danger.route.ts +++ b/packages/vitnode/src/api/modules/admin/notifications/routes/danger.route.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { cancelQueuedNotificationEmails, deleteAllNotifications, @@ -9,7 +9,6 @@ import { resetMemberNotificationPreferences, resumeNotifications, } from "@/api/models/notifications/danger"; -import { CONFIG_PLUGIN } from "@/config"; const canManageNotifications = { module: "notifications", @@ -23,7 +22,6 @@ const dangerResponses = (schema: T, description: string) => }) as const; export const pauseNotificationsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: canManageNotifications, route: { method: "post", @@ -40,7 +38,6 @@ export const pauseNotificationsRoute = buildRoute({ }); export const resumeNotificationsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: canManageNotifications, route: { method: "post", @@ -56,7 +53,6 @@ export const resumeNotificationsRoute = buildRoute({ }); export const cancelQueuedNotificationEmailsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: canManageNotifications, route: { method: "post", @@ -72,7 +68,6 @@ export const cancelQueuedNotificationEmailsRoute = buildRoute({ }); export const markEverythingReadRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: canManageNotifications, route: { method: "post", @@ -88,7 +83,6 @@ export const markEverythingReadRoute = buildRoute({ }); export const deleteAllNotificationsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: canManageNotifications, route: { method: "post", @@ -104,7 +98,6 @@ export const deleteAllNotificationsRoute = buildRoute({ }); export const resetMemberNotificationPreferencesRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: canManageNotifications, route: { method: "post", diff --git a/packages/vitnode/src/api/modules/admin/notifications/routes/deliveries.route.ts b/packages/vitnode/src/api/modules/admin/notifications/routes/deliveries.route.ts index 68c824e2e7..91ba2ddf09 100644 --- a/packages/vitnode/src/api/modules/admin/notifications/routes/deliveries.route.ts +++ b/packages/vitnode/src/api/modules/admin/notifications/routes/deliveries.route.ts @@ -1,11 +1,10 @@ import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { listNotificationDeliveries, retryNotificationDelivery, } from "@/api/models/notifications/admin"; -import { CONFIG_PLUGIN } from "@/config"; import { core_notification_deliveries } from "@/database/notifications"; const zodDeliveryStatus = z.enum( @@ -13,7 +12,6 @@ const zodDeliveryStatus = z.enum( ); export const listNotificationDeliveriesRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "notifications", permission: "can_view" }, route: { method: "get", @@ -70,7 +68,6 @@ export const listNotificationDeliveriesRoute = buildRoute({ }); export const retryNotificationDeliveryRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "notifications", permission: "can_manage" }, route: { method: "post", diff --git a/packages/vitnode/src/api/modules/admin/notifications/routes/maintenance.route.ts b/packages/vitnode/src/api/modules/admin/notifications/routes/maintenance.route.ts index 0b1204415f..0b27e3e61c 100644 --- a/packages/vitnode/src/api/modules/admin/notifications/routes/maintenance.route.ts +++ b/packages/vitnode/src/api/modules/admin/notifications/routes/maintenance.route.ts @@ -1,15 +1,13 @@ import { HTTPException } from "hono/http-exception"; import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { queueNotificationTestEmail, reconcileNotifications, } from "@/api/models/notifications/admin"; -import { CONFIG_PLUGIN } from "@/config"; export const sendNotificationTestEmailRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "notifications", permission: "can_manage" }, route: { method: "post", @@ -38,7 +36,6 @@ export const sendNotificationTestEmailRoute = buildRoute({ }); export const reconcileNotificationCountsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "notifications", permission: "can_manage" }, route: { method: "post", diff --git a/packages/vitnode/src/api/modules/admin/notifications/routes/overview.route.ts b/packages/vitnode/src/api/modules/admin/notifications/routes/overview.route.ts index e26b8b3650..69f98b2a25 100644 --- a/packages/vitnode/src/api/modules/admin/notifications/routes/overview.route.ts +++ b/packages/vitnode/src/api/modules/admin/notifications/routes/overview.route.ts @@ -1,14 +1,12 @@ import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { getNotificationsOverview } from "@/api/models/notifications/admin"; import { zodNotificationEmailMode } from "@/api/modules/notifications/schema"; -import { CONFIG_PLUGIN } from "@/config"; const zodCounts = z.record(z.string(), z.number()); export const getNotificationsOverviewRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "notifications", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/notifications/routes/settings.route.ts b/packages/vitnode/src/api/modules/admin/notifications/routes/settings.route.ts index 4139c20477..2758540faf 100644 --- a/packages/vitnode/src/api/modules/admin/notifications/routes/settings.route.ts +++ b/packages/vitnode/src/api/modules/admin/notifications/routes/settings.route.ts @@ -1,12 +1,10 @@ import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { updateNotificationTypePolicy } from "@/api/models/notifications/admin"; import { zodNotificationEmailMode } from "@/api/modules/notifications/schema"; -import { CONFIG_PLUGIN } from "@/config"; export const updateNotificationTypePolicyRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "notifications", permission: "can_edit" }, route: { method: "put", diff --git a/packages/vitnode/src/api/modules/admin/notifications/routes/stats.route.ts b/packages/vitnode/src/api/modules/admin/notifications/routes/stats.route.ts index be886536d4..640239547a 100644 --- a/packages/vitnode/src/api/modules/admin/notifications/routes/stats.route.ts +++ b/packages/vitnode/src/api/modules/admin/notifications/routes/stats.route.ts @@ -1,13 +1,12 @@ import { HTTPException } from "hono/http-exception"; import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { isValidTimeZone } from "@/api/lib/notifications/digest-period"; -import { buildRoute } from "@/api/lib/route"; import { getNotificationStats, NOTIFICATION_STATS_RANGES, } from "@/api/models/notifications/stats"; -import { CONFIG_PLUGIN } from "@/config"; const zodTotals = z.object({ events: z.number(), @@ -17,7 +16,6 @@ const zodTotals = z.object({ }); export const getNotificationStatsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "notifications", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/plugins/plugins.admin.module.ts b/packages/vitnode/src/api/modules/admin/plugins/plugins.admin.module.ts new file mode 100644 index 0000000000..c31eadd91b --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/plugins/plugins.admin.module.ts @@ -0,0 +1,8 @@ +import { buildModule } from "@/api/builders"; + +import { listPluginsAdminRoute } from "./routes/list.route"; + +export const pluginsAdminModule = buildModule({ + name: "plugins", + routes: [listPluginsAdminRoute], +}); diff --git a/packages/vitnode/src/api/modules/admin/plugins/routes/list.route.test.ts b/packages/vitnode/src/api/modules/admin/plugins/routes/list.route.test.ts new file mode 100644 index 0000000000..dd815b80d2 --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/plugins/routes/list.route.test.ts @@ -0,0 +1,86 @@ +import type { Context } from "hono"; + +import { OpenAPIHono } from "@hono/zod-openapi"; +import { describe, expect, it } from "vitest"; + +import type { PermissionsStaffArgs } from "@/api/lib/permission-staff"; + +import { buildInstalledPluginRegistry } from "@/api/lib/installed-plugins"; +import { manifest } from "@/manifest"; +import { createTestCache } from "@/tests/cache"; +import { grantStaffPermissions } from "@/tests/staff-permissions"; + +import { listPluginsAdminRoute } from "./list.route"; + +const ADMIN = { id: 1, roleId: 1 }; + +const installedPlugins = buildInstalledPluginRegistry({ + api: [ + { + manifest: { + author: null, + description: "Posts and categories", + icon: "newspaper", + id: "@acme/blog", + license: null, + title: "Blog", + version: "2.0.0", + }, + pluginId: "@acme/blog", + }, + ], + frontend: [ + { + id: "@acme/blog", + manifest: { + author: null, + description: "Posts and categories", + icon: "newspaper", + id: "@acme/blog", + license: null, + title: "Blog", + version: "1.0.0", + }, + }, + { id: "@acme/analytics", manifest: null }, + ], +}); + +const listAs = async (permissions: PermissionsStaffArgs[]) => { + const cache = createTestCache(); + await grantStaffPermissions(cache, { permissions, userId: ADMIN.id }); + + const app = new OpenAPIHono(); + app.use("*", async (c, next) => { + c.set("admin", { user: ADMIN } as unknown as Context["var"]["admin"]); + c.set("cache", cache); + c.set("core", { installedPlugins } as unknown as Context["var"]["core"]); + await next(); + }); + app.openapi(listPluginsAdminRoute.route, listPluginsAdminRoute.handler); + + return await app.request("/"); +}; + +describe("GET /admin/plugins", () => { + it("lists the installed plugins and the core version", async () => { + const response = await listAs([ + { module: "plugins", permission: "can_view", plugin: "@vitnode/core" }, + ]); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + core: { version: manifest.version }, + frontendInventory: true, + plugins: installedPlugins.plugins, + }); + }); + + it("refuses an administrator without the plugins permission", async () => { + const response = await listAs([ + { module: "system", permission: "can_view", plugin: "@vitnode/core" }, + ]); + + expect(response.status).toBe(403); + }); +}); diff --git a/packages/vitnode/src/api/modules/admin/plugins/routes/list.route.ts b/packages/vitnode/src/api/modules/admin/plugins/routes/list.route.ts new file mode 100644 index 0000000000..23215de054 --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/plugins/routes/list.route.ts @@ -0,0 +1,57 @@ +import { z } from "zod"; + +import { buildRoute } from "@/api/builders"; +import { manifest } from "@/manifest"; + +const registrationSchema = z + .object({ + version: z.string().nullable(), + }) + .nullable(); + +export const listPluginsAdminRoute = buildRoute({ + adminStaffPermission: { module: "plugins", permission: "can_view" }, + route: { + method: "get", + description: + "The plugins this application is configured with, from its build-generated metadata", + path: "/", + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ + core: z.object({ version: z.string() }), + frontendInventory: z.boolean(), + plugins: z.array( + z.object({ + id: z.string(), + title: z.string(), + icon: z.string().nullable(), + description: z.string().nullable(), + version: z.string().nullable(), + versionMismatch: z.boolean(), + registrations: z.object({ + api: registrationSchema, + frontend: registrationSchema, + }), + }), + ), + }), + }, + }, + description: "Installed plugins", + }, + 403: { description: "Access Denied" }, + }, + }, + handler: c => { + const { frontendInventory, plugins } = c.get("core").installedPlugins; + + return c.json({ + core: { version: manifest.version }, + frontendInventory, + plugins, + }); + }, +}); diff --git a/packages/vitnode/src/api/modules/admin/roles/lib/assert-can-list-roles.ts b/packages/vitnode/src/api/modules/admin/roles/lib/assert-can-list-roles.ts index b71d68c690..fcaf6a5634 100644 --- a/packages/vitnode/src/api/modules/admin/roles/lib/assert-can-list-roles.ts +++ b/packages/vitnode/src/api/modules/admin/roles/lib/assert-can-list-roles.ts @@ -4,7 +4,7 @@ import { HTTPException } from "hono/http-exception"; import { resolveStaffPermissions } from "@/api/lib/check-staff-permission"; import { hasStaffPermission } from "@/api/lib/staff-permission"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; const ROLE_LIST_READERS = [ { module: "roles", permission: "can_view" }, @@ -22,7 +22,7 @@ export const assertCanListRoles = async (c: Context): Promise => { const permissions = await resolveStaffPermissions(c, { type: "admin", user }); const allowed = ROLE_LIST_READERS.some(reader => hasStaffPermission(permissions, { - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, ...reader, }), ); diff --git a/packages/vitnode/src/api/modules/admin/roles/lib/assert-manage-admin-role.ts b/packages/vitnode/src/api/modules/admin/roles/lib/assert-manage-admin-role.ts index 09cf601dda..151544e427 100644 --- a/packages/vitnode/src/api/modules/admin/roles/lib/assert-manage-admin-role.ts +++ b/packages/vitnode/src/api/modules/admin/roles/lib/assert-manage-admin-role.ts @@ -3,10 +3,10 @@ import type { Context } from "hono"; import { and, eq } from "drizzle-orm"; import { assertStaffPermission } from "@/api/lib/check-staff-permission"; -import { CONFIG_PLUGIN } from "@/config"; import { core_admin_permissions } from "@/database/admins"; import { core_moderators_permissions } from "@/database/moderators"; import { core_roles } from "@/database/roles"; +import { manifest } from "@/manifest"; const isStaffRole = async (c: Context, roleId: number): Promise => { const db = c.get("db"); @@ -50,7 +50,7 @@ export const assertCanManageAdminRole = async ( await assertStaffPermission(c, { type: "admin", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, module: "roles", permission, }); diff --git a/packages/vitnode/src/api/modules/admin/roles/roles.admin.module.ts b/packages/vitnode/src/api/modules/admin/roles/roles.admin.module.ts index 9f60628126..465b1f6eaf 100644 --- a/packages/vitnode/src/api/modules/admin/roles/roles.admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/roles/roles.admin.module.ts @@ -1,5 +1,4 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { createRoleAdminRoute } from "./routes/create.route"; import { deleteRoleAdminRoute } from "./routes/delete.route"; @@ -8,7 +7,6 @@ import { showRoleAdminRoute } from "./routes/show.route"; import { updateRoleAdminRoute } from "./routes/update.route"; export const rolesAdminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "roles", routes: [ listRolesAdminRoute, diff --git a/packages/vitnode/src/api/modules/admin/roles/routes/create.route.ts b/packages/vitnode/src/api/modules/admin/roles/routes/create.route.ts index 748e9f4350..06154e065a 100644 --- a/packages/vitnode/src/api/modules/admin/roles/routes/create.route.ts +++ b/packages/vitnode/src/api/modules/admin/roles/routes/create.route.ts @@ -1,8 +1,7 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { saveLanguageWords } from "@/api/lib/save-language-words"; -import { CONFIG_PLUGIN } from "@/config"; import { core_roles } from "@/database/roles"; import { EMOJI_ICON_MAX_LENGTH, @@ -48,7 +47,6 @@ export const zodCreateRoleAdminSchema = z.object({ }); export const createRoleAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "roles", permission: "can_create" }, route: { method: "post", diff --git a/packages/vitnode/src/api/modules/admin/roles/routes/delete.route.ts b/packages/vitnode/src/api/modules/admin/roles/routes/delete.route.ts index 9ca8da378d..187074882e 100644 --- a/packages/vitnode/src/api/modules/admin/roles/routes/delete.route.ts +++ b/packages/vitnode/src/api/modules/admin/roles/routes/delete.route.ts @@ -1,11 +1,10 @@ import { z } from "@hono/zod-openapi"; import { and, count, eq, inArray } from "drizzle-orm"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { invalidateAllStaffPermissions } from "@/api/lib/staff-permission-cache"; import { invalidateSessionCacheForUser } from "@/api/models/session-revoke"; import { assertCanAssignPrimaryRole } from "@/api/modules/admin/users/lib/assert-edit-user-permission"; -import { CONFIG_PLUGIN } from "@/config"; import { core_languages_words } from "@/database/languages"; import { core_roles } from "@/database/roles"; import { core_users, core_users_secondary_roles } from "@/database/users"; @@ -13,7 +12,6 @@ import { core_users, core_users_secondary_roles } from "@/database/users"; import { assertCanManageAdminRole } from "../lib/assert-manage-admin-role"; export const deleteRoleAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "roles", permission: "can_delete" }, route: { method: "delete", diff --git a/packages/vitnode/src/api/modules/admin/roles/routes/list.route.ts b/packages/vitnode/src/api/modules/admin/roles/routes/list.route.ts index edda5d6b5f..0778dc1588 100644 --- a/packages/vitnode/src/api/modules/admin/roles/routes/list.route.ts +++ b/packages/vitnode/src/api/modules/admin/roles/routes/list.route.ts @@ -1,13 +1,12 @@ import { z } from "@hono/zod-openapi"; import { and, count, eq, ilike, inArray, ne } from "drizzle-orm"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { withPagination, zodPaginationPageInfo, zodPaginationQuery, } from "@/api/lib/with-pagination"; -import { CONFIG_PLUGIN } from "@/config"; import { core_admin_permissions } from "@/database/admins"; import { core_languages_words } from "@/database/languages"; import { core_moderators_permissions } from "@/database/moderators"; @@ -54,7 +53,6 @@ const rolesAdminListSchema = z.object({ }); export const listRolesAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: "Get list of all roles (Admin only)", diff --git a/packages/vitnode/src/api/modules/admin/roles/routes/show.route.ts b/packages/vitnode/src/api/modules/admin/roles/routes/show.route.ts index dcc0fa1979..23e2b0a334 100644 --- a/packages/vitnode/src/api/modules/admin/roles/routes/show.route.ts +++ b/packages/vitnode/src/api/modules/admin/roles/routes/show.route.ts @@ -1,8 +1,7 @@ import { z } from "@hono/zod-openapi"; import { and, eq } from "drizzle-orm"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { core_languages_words } from "@/database/languages"; import { core_roles } from "@/database/roles"; @@ -35,7 +34,6 @@ const roleAdminSchema = z.object({ }); export const showRoleAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "roles", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/roles/routes/update.route.ts b/packages/vitnode/src/api/modules/admin/roles/routes/update.route.ts index 8541ae6673..b36aa6c10f 100644 --- a/packages/vitnode/src/api/modules/admin/roles/routes/update.route.ts +++ b/packages/vitnode/src/api/modules/admin/roles/routes/update.route.ts @@ -1,10 +1,9 @@ import { z } from "@hono/zod-openapi"; import { eq } from "drizzle-orm"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { saveLanguageWords } from "@/api/lib/save-language-words"; import { invalidateAllStaffPermissions } from "@/api/lib/staff-permission-cache"; -import { CONFIG_PLUGIN } from "@/config"; import { core_roles } from "@/database/roles"; import { parseEmojiIcon, serializeEmojiIcon } from "@/lib/emoji-icon"; @@ -36,7 +35,6 @@ export const zodUpdateRoleAdminSchema = z }); export const updateRoleAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "roles", permission: "can_edit" }, route: { method: "patch", diff --git a/packages/vitnode/src/api/modules/admin/routes/notifications.route.ts b/packages/vitnode/src/api/modules/admin/routes/notifications.route.ts index 852bf711e8..a2f8913de0 100644 --- a/packages/vitnode/src/api/modules/admin/routes/notifications.route.ts +++ b/packages/vitnode/src/api/modules/admin/routes/notifications.route.ts @@ -1,9 +1,8 @@ import { HTTPException } from "hono/http-exception"; import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { adminMessageNotification } from "@/api/lib/notifications/core-types"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { notificationsChannel } from "@/ws/notifications"; export const zodSendNotificationSchema = z.object({ @@ -14,7 +13,6 @@ export const zodSendNotificationSchema = z.object({ }); export const sendNotificationRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, // The send-notification dashboard widget is the only caller, and its sibling // (`PUT /admin/dashboard/widget-settings`) is gated the same way. Without a // declaration the route asked only for *an* admin session, so an diff --git a/packages/vitnode/src/api/modules/admin/routes/session.route.ts b/packages/vitnode/src/api/modules/admin/routes/session.route.ts index 4409a36d74..af5482b941 100644 --- a/packages/vitnode/src/api/modules/admin/routes/session.route.ts +++ b/packages/vitnode/src/api/modules/admin/routes/session.route.ts @@ -1,12 +1,11 @@ import { HTTPException } from "hono/http-exception"; import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { resolveStaffPermissions } from "@/api/lib/check-staff-permission"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; export const sessionAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: "Verify admin session", @@ -71,7 +70,7 @@ export const sessionAdminRoute = buildRoute({ return c.json({ user, permissions, - vitnode_version: CONFIG_PLUGIN.version, + vitnode_version: manifest.version, expiresAt, signOutWhenTabsClose: c.get("core").authorization.cookieDomain === undefined, diff --git a/packages/vitnode/src/api/modules/admin/staff/routes/admins.route.ts b/packages/vitnode/src/api/modules/admin/staff/routes/admins.route.ts index cdea9d1946..324da390fd 100644 --- a/packages/vitnode/src/api/modules/admin/staff/routes/admins.route.ts +++ b/packages/vitnode/src/api/modules/admin/staff/routes/admins.route.ts @@ -1,13 +1,11 @@ -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { withPagination } from "@/api/lib/with-pagination"; -import { CONFIG_PLUGIN } from "@/config"; import { core_admin_permissions } from "@/database/admins"; import { resolveStaffEdges } from "../lib/resolve-staff-edges"; import { staffListAdminQuery, staffListAdminSchema } from "../lib/schema"; export const listAdminsStaffAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "staff_admins", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/staff/routes/create.route.ts b/packages/vitnode/src/api/modules/admin/staff/routes/create.route.ts index 24e98666c3..1bd421e10e 100644 --- a/packages/vitnode/src/api/modules/admin/staff/routes/create.route.ts +++ b/packages/vitnode/src/api/modules/admin/staff/routes/create.route.ts @@ -1,12 +1,12 @@ import { z } from "@hono/zod-openapi"; import { eq, or } from "drizzle-orm"; +import { buildRoute } from "@/api/builders"; import { assertStaffPermission } from "@/api/lib/check-staff-permission"; -import { buildRoute } from "@/api/lib/route"; import { invalidateStaffEntry } from "@/api/lib/staff-permission-cache"; -import { CONFIG_PLUGIN } from "@/config"; import { core_admin_permissions } from "@/database/admins"; import { core_moderators_permissions } from "@/database/moderators"; +import { manifest } from "@/manifest"; import { staffPermissionModuleByType, staffTypeSchema } from "../lib/schema"; import { @@ -20,7 +20,6 @@ const tableByType = { } as const; export const createStaffAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: "Create a staff entry for a role or a user (Admin only)", @@ -78,7 +77,7 @@ export const createStaffAdminRoute = buildRoute({ const { type } = c.req.valid("param"); await assertStaffPermission(c, { type: "admin", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, module: staffPermissionModuleByType[type], permission: "can_create", }); diff --git a/packages/vitnode/src/api/modules/admin/staff/routes/delete.route.ts b/packages/vitnode/src/api/modules/admin/staff/routes/delete.route.ts index 48203cbb4c..68185a7aef 100644 --- a/packages/vitnode/src/api/modules/admin/staff/routes/delete.route.ts +++ b/packages/vitnode/src/api/modules/admin/staff/routes/delete.route.ts @@ -2,12 +2,12 @@ import { z } from "@hono/zod-openapi"; import { eq } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; +import { buildRoute } from "@/api/builders"; import { assertStaffPermission } from "@/api/lib/check-staff-permission"; -import { buildRoute } from "@/api/lib/route"; import { invalidateStaffEntry } from "@/api/lib/staff-permission-cache"; -import { CONFIG_PLUGIN } from "@/config"; import { core_admin_permissions } from "@/database/admins"; import { core_moderators_permissions } from "@/database/moderators"; +import { manifest } from "@/manifest"; import { staffPermissionModuleByType, staffTypeSchema } from "../lib/schema"; import { @@ -21,7 +21,6 @@ const tableByType = { } as const; export const deleteStaffAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "delete", description: "Remove a staff entry (Admin only)", @@ -53,7 +52,7 @@ export const deleteStaffAdminRoute = buildRoute({ const { type, id } = c.req.valid("param"); await assertStaffPermission(c, { type: "admin", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, module: staffPermissionModuleByType[type], permission: "can_delete", }); diff --git a/packages/vitnode/src/api/modules/admin/staff/routes/moderators.route.ts b/packages/vitnode/src/api/modules/admin/staff/routes/moderators.route.ts index d95e971151..6f2a0eaceb 100644 --- a/packages/vitnode/src/api/modules/admin/staff/routes/moderators.route.ts +++ b/packages/vitnode/src/api/modules/admin/staff/routes/moderators.route.ts @@ -1,13 +1,11 @@ -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { withPagination } from "@/api/lib/with-pagination"; -import { CONFIG_PLUGIN } from "@/config"; import { core_moderators_permissions } from "@/database/moderators"; import { resolveStaffEdges } from "../lib/resolve-staff-edges"; import { staffListAdminQuery, staffListAdminSchema } from "../lib/schema"; export const listModeratorsStaffAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "staff_moderators", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/staff/routes/permission-catalog.route.ts b/packages/vitnode/src/api/modules/admin/staff/routes/permission-catalog.route.ts index f48c8ba389..14570dd183 100644 --- a/packages/vitnode/src/api/modules/admin/staff/routes/permission-catalog.route.ts +++ b/packages/vitnode/src/api/modules/admin/staff/routes/permission-catalog.route.ts @@ -1,7 +1,6 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; const permissionStaffModulesSchema = z.record( z.string(), @@ -21,7 +20,6 @@ const scopeFamilySchema = z.object({ }); export const permissionCatalogStaffAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: diff --git a/packages/vitnode/src/api/modules/admin/staff/routes/scope-resources.route.ts b/packages/vitnode/src/api/modules/admin/staff/routes/scope-resources.route.ts index c23fa2e6d0..9c4eabc681 100644 --- a/packages/vitnode/src/api/modules/admin/staff/routes/scope-resources.route.ts +++ b/packages/vitnode/src/api/modules/admin/staff/routes/scope-resources.route.ts @@ -1,9 +1,8 @@ import { z } from "@hono/zod-openapi"; +import { buildRoute } from "@/api/builders"; import { findModerationScope } from "@/api/lib/moderation/registry"; import { isModerationResourceId } from "@/api/lib/moderation/scopes"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { staffPermissionModuleByType } from "../lib/schema"; @@ -16,7 +15,6 @@ const resourceSchema = z.object({ const PAGE_SIZE = 20; export const scopeResourcesStaffAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: staffPermissionModuleByType.moderator, permission: "can_edit", diff --git a/packages/vitnode/src/api/modules/admin/staff/routes/show-permissions.route.ts b/packages/vitnode/src/api/modules/admin/staff/routes/show-permissions.route.ts index 40ce5ea2d9..8cdff418f3 100644 --- a/packages/vitnode/src/api/modules/admin/staff/routes/show-permissions.route.ts +++ b/packages/vitnode/src/api/modules/admin/staff/routes/show-permissions.route.ts @@ -1,11 +1,11 @@ import { z } from "@hono/zod-openapi"; import { eq } from "drizzle-orm"; +import { buildRoute } from "@/api/builders"; import { assertStaffPermission } from "@/api/lib/check-staff-permission"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { core_admin_permissions } from "@/database/admins"; import { core_moderators_permissions } from "@/database/moderators"; +import { manifest } from "@/manifest"; import { resolveStaffEdges } from "../lib/resolve-staff-edges"; import { @@ -21,7 +21,6 @@ const tableByType = { } as const; export const showPermissionsStaffAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: @@ -61,7 +60,7 @@ export const showPermissionsStaffAdminRoute = buildRoute({ const { type, id } = c.req.valid("param"); await assertStaffPermission(c, { type: "admin", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, module: staffPermissionModuleByType[type], permission: "can_edit", }); diff --git a/packages/vitnode/src/api/modules/admin/staff/routes/update-permissions.route.ts b/packages/vitnode/src/api/modules/admin/staff/routes/update-permissions.route.ts index 1baaaccce0..d0c8b3fa4b 100644 --- a/packages/vitnode/src/api/modules/admin/staff/routes/update-permissions.route.ts +++ b/packages/vitnode/src/api/modules/admin/staff/routes/update-permissions.route.ts @@ -4,12 +4,12 @@ import { HTTPException } from "hono/http-exception"; import type { PermissionsStaffArgs } from "@/api/lib/permission-staff"; +import { buildRoute } from "@/api/builders"; import { assertStaffPermission } from "@/api/lib/check-staff-permission"; -import { buildRoute } from "@/api/lib/route"; import { invalidateStaffEntry } from "@/api/lib/staff-permission-cache"; -import { CONFIG_PLUGIN } from "@/config"; import { core_admin_permissions } from "@/database/admins"; import { core_moderators_permissions } from "@/database/moderators"; +import { manifest } from "@/manifest"; import { permissionsStaffArgsSchema, @@ -30,7 +30,6 @@ const tableByType = { } as const; export const updatePermissionsStaffAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "patch", description: "Update the granted permissions of a staff entry (Admin only)", @@ -92,7 +91,7 @@ export const updatePermissionsStaffAdminRoute = buildRoute({ const { type, id } = c.req.valid("param"); await assertStaffPermission(c, { type: "admin", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, module: staffPermissionModuleByType[type], permission: "can_edit", }); diff --git a/packages/vitnode/src/api/modules/admin/staff/staff.admin.module.ts b/packages/vitnode/src/api/modules/admin/staff/staff.admin.module.ts index e1320d356e..ef43007553 100644 --- a/packages/vitnode/src/api/modules/admin/staff/staff.admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/staff/staff.admin.module.ts @@ -1,5 +1,4 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { listAdminsStaffAdminRoute } from "./routes/admins.route"; import { createStaffAdminRoute } from "./routes/create.route"; @@ -11,7 +10,6 @@ import { showPermissionsStaffAdminRoute } from "./routes/show-permissions.route" import { updatePermissionsStaffAdminRoute } from "./routes/update-permissions.route"; export const staffAdminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "staff", routes: [ listModeratorsStaffAdminRoute, diff --git a/packages/vitnode/src/api/modules/admin/users/lib/assert-edit-user-permission.ts b/packages/vitnode/src/api/modules/admin/users/lib/assert-edit-user-permission.ts index e6bf97d171..b36b45737d 100644 --- a/packages/vitnode/src/api/modules/admin/users/lib/assert-edit-user-permission.ts +++ b/packages/vitnode/src/api/modules/admin/users/lib/assert-edit-user-permission.ts @@ -15,10 +15,10 @@ import { resolveStaffPermissions, } from "@/api/lib/check-staff-permission"; import { hasStaffPermission } from "@/api/lib/staff-permission"; -import { CONFIG_PLUGIN } from "@/config"; import { core_admin_permissions } from "@/database/admins"; import { core_moderators_permissions } from "@/database/moderators"; import { core_roles } from "@/database/roles"; +import { manifest } from "@/manifest"; const STAFF_TYPES: PermissionStaffType[] = ["admin", "moderator"]; @@ -38,7 +38,7 @@ const forbidden = () => new HTTPException(403, { message: "Forbidden" }); const assertCanEditAdmin = async (c: Context): Promise => { await assertStaffPermission(c, { type: "admin", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, module: "users", permission: "can_edit_admin", }); diff --git a/packages/vitnode/src/api/modules/admin/users/routes/create.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/create.route.ts index 03a84e6e4e..ead25c3fb2 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/create.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/create.route.ts @@ -1,6 +1,6 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { PasswordModel } from "@/api/models/password"; import { UserModel } from "@/api/models/user"; import { @@ -8,7 +8,6 @@ import { USER_NAME_MAX_LENGTH, USER_PASSWORD_MAX_LENGTH, } from "@/api/modules/users/credential-limits"; -import { CONFIG_PLUGIN } from "@/config"; const nameRegex = /^(?!.* {2})[\p{L}\p{N}._@ -]*$/u; @@ -30,7 +29,6 @@ export const zodCreateUserAdminSchema = z.object({ }); export const createUserAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_create" }, route: { method: "post", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/devices.route.test.ts b/packages/vitnode/src/api/modules/admin/users/routes/devices.route.test.ts index b0f9e04c39..47cd5d56d2 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/devices.route.test.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/devices.route.test.ts @@ -5,12 +5,11 @@ import { describe, expect, it, vi } from "vitest"; import type { PermissionsStaffArgs } from "@/api/lib/permission-staff"; -import { buildModule } from "@/api/lib/module"; +import { buildModule } from "@/api/builders"; import { adminSessionCacheKey, sessionCacheKey, } from "@/api/models/session-cache"; -import { CONFIG_PLUGIN } from "@/config"; import { core_admin_permissions, core_admin_sessions } from "@/database/admins"; import { core_moderators_permissions } from "@/database/moderators"; import { core_roles } from "@/database/roles"; @@ -139,7 +138,6 @@ const harness = async (editor: PermissionsStaffArgs[]) => { "/", buildModule({ name: "users", - pluginId: CONFIG_PLUGIN.pluginId, routes: [ listUserDevicesAdminRoute, revokeUserDevicesAdminRoute, diff --git a/packages/vitnode/src/api/modules/admin/users/routes/devices.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/devices.route.ts index 78084dd8c4..e4a1609b96 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/devices.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/devices.route.ts @@ -1,11 +1,10 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { listUserDevices, zodUserDeviceSchema, } from "@/api/models/user-devices"; -import { CONFIG_PLUGIN } from "@/config"; import { findTargetUserId, @@ -15,7 +14,6 @@ import { } from "../lib/target-user"; export const listUserDevicesAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/image-delete.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/image-delete.route.ts index 64b3651bbf..1c3152e550 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/image-delete.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/image-delete.route.ts @@ -1,16 +1,14 @@ import { z } from "@hono/zod-openapi"; import { eq } from "drizzle-orm"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { removeUserImage, zodUserImageKind } from "@/api/lib/user-images"; import { SsoConnectionModel } from "@/api/models/sso-connection"; -import { CONFIG_PLUGIN } from "@/config"; import { core_users } from "@/database/users"; import { assertCanEditAdminTarget } from "../lib/assert-edit-user-permission"; export const deleteUserImageAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_edit" }, route: { method: "delete", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/image-upload.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/image-upload.route.ts index cb9a2316c3..1f30eace21 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/image-upload.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/image-upload.route.ts @@ -2,20 +2,18 @@ import { z } from "@hono/zod-openapi"; import { eq } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { resolveUserImagePolicy, setUserImage, zodUserImageKind, } from "@/api/lib/user-images"; import { SsoConnectionModel } from "@/api/models/sso-connection"; -import { CONFIG_PLUGIN } from "@/config"; import { core_users } from "@/database/users"; import { assertCanEditAdminTarget } from "../lib/assert-edit-user-permission"; export const uploadUserImageAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_edit" }, route: { method: "post", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/list.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/list.route.ts index 411f8053ad..1ce11b9903 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/list.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/list.route.ts @@ -2,15 +2,14 @@ import { z } from "@hono/zod-openapi"; import { eq, inArray } from "drizzle-orm"; import { alias } from "drizzle-orm/pg-core"; +import { buildRoute } from "@/api/builders"; import { resolveRoleNames } from "@/api/lib/resolve-role-names"; -import { buildRoute } from "@/api/lib/route"; import { storageUrlOf } from "@/api/lib/storage-url"; import { withPagination, zodPaginationPageInfo, zodPaginationQuery, } from "@/api/lib/with-pagination"; -import { CONFIG_PLUGIN } from "@/config"; import { core_files } from "@/database/files"; import { core_roles } from "@/database/roles"; import { core_users, core_users_secondary_roles } from "@/database/users"; @@ -18,7 +17,6 @@ import { core_users, core_users_secondary_roles } from "@/database/users"; const avatarFile = alias(core_files, "user_avatar_file"); export const listUsersAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/notification-preferences-update.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/notification-preferences-update.route.ts index 74ad192406..d93c929f70 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/notification-preferences-update.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/notification-preferences-update.route.ts @@ -1,4 +1,4 @@ -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { getNotificationPreferences, updateNotificationPreferences, @@ -7,7 +7,6 @@ import { zodNotificationPreferences, zodUpdateNotificationPreferences, } from "@/api/modules/notifications/routes/preferences.route"; -import { CONFIG_PLUGIN } from "@/config"; import { assertCanEditAdminTarget } from "../lib/assert-edit-user-permission"; import { @@ -17,7 +16,6 @@ import { } from "../lib/target-user"; export const updateUserNotificationPreferencesAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_edit" }, route: { method: "put", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/notification-preferences.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/notification-preferences.route.ts index f76466e00d..5f5ca31cf4 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/notification-preferences.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/notification-preferences.route.ts @@ -1,7 +1,6 @@ -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { getNotificationPreferences } from "@/api/models/notifications/preferences"; import { zodNotificationPreferences } from "@/api/modules/notifications/routes/preferences.route"; -import { CONFIG_PLUGIN } from "@/config"; import { findTargetUserId, @@ -10,7 +9,6 @@ import { } from "../lib/target-user"; export const listUserNotificationPreferencesAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/passkey-delete.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/passkey-delete.route.ts index d8e2327a09..2710e7c65b 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/passkey-delete.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/passkey-delete.route.ts @@ -1,13 +1,12 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { PasskeyModel } from "@/api/models/passkey"; import { passkeyFailure } from "@/api/modules/users/passkeys/failure"; import { PASSKEY_ERROR_RESPONSES, zodPasskeyIdParam, } from "@/api/modules/users/passkeys/schema"; -import { CONFIG_PLUGIN } from "@/config"; import { assertCanEditAdminTarget } from "../lib/assert-edit-user-permission"; import { @@ -18,7 +17,6 @@ import { } from "../lib/target-user"; export const deleteUserPasskeyAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_edit" }, route: { method: "delete", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/passkey-rename.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/passkey-rename.route.ts index 6810fca748..b17c4ef93e 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/passkey-rename.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/passkey-rename.route.ts @@ -1,6 +1,6 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { PasskeyModel } from "@/api/models/passkey"; import { passkeyFailure } from "@/api/modules/users/passkeys/failure"; import { @@ -9,7 +9,6 @@ import { zodPasskeyNameSchema, zodPasskeySchema, } from "@/api/modules/users/passkeys/schema"; -import { CONFIG_PLUGIN } from "@/config"; import { assertCanEditAdminTarget } from "../lib/assert-edit-user-permission"; import { @@ -20,7 +19,6 @@ import { } from "../lib/target-user"; export const renameUserPasskeyAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_edit" }, route: { method: "patch", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/passkeys.route.test.ts b/packages/vitnode/src/api/modules/admin/users/routes/passkeys.route.test.ts index 6d6b1d25ec..906e55d4a0 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/passkeys.route.test.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/passkeys.route.test.ts @@ -6,9 +6,8 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import type { PermissionsStaffArgs } from "@/api/lib/permission-staff"; import type { EnvVariablesVitNode } from "@/api/middlewares/global.middleware"; -import { buildModule } from "@/api/lib/module"; +import { buildModule } from "@/api/builders"; import { PasskeyModel } from "@/api/models/passkey"; -import { CONFIG_PLUGIN } from "@/config"; import { core_admin_permissions } from "@/database/admins"; import { core_moderators_permissions } from "@/database/moderators"; import { core_roles } from "@/database/roles"; @@ -142,7 +141,6 @@ const harness = async ({ "/", buildModule({ name: "users", - pluginId: CONFIG_PLUGIN.pluginId, routes: [ listUserPasskeysAdminRoute, renameUserPasskeyAdminRoute, diff --git a/packages/vitnode/src/api/modules/admin/users/routes/passkeys.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/passkeys.route.ts index 8feae4748e..e108bd7749 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/passkeys.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/passkeys.route.ts @@ -1,13 +1,12 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { PasskeyModel } from "@/api/models/passkey"; import { passkeyFailure } from "@/api/modules/users/passkeys/failure"; import { PASSKEY_ERROR_RESPONSES, zodPasskeySchema, } from "@/api/modules/users/passkeys/schema"; -import { CONFIG_PLUGIN } from "@/config"; import { findTargetUserId, @@ -17,7 +16,6 @@ import { } from "../lib/target-user"; export const listUserPasskeysAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/password.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/password.route.ts index b185c68eb8..69de43dbc1 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/password.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/password.route.ts @@ -1,12 +1,11 @@ import { z } from "@hono/zod-openapi"; import { eq } from "drizzle-orm"; +import { buildRoute } from "@/api/builders"; import { assertPasswordSignInEnabled } from "@/api/lib/password-sign-in"; -import { buildRoute } from "@/api/lib/route"; import { PasswordModel } from "@/api/models/password"; import { revokeSessions } from "@/api/models/session-revoke"; import { USER_PASSWORD_MAX_LENGTH } from "@/api/modules/users/credential-limits"; -import { CONFIG_PLUGIN } from "@/config"; import { core_users } from "@/database/users"; import { assertCanEditAdminTarget } from "../lib/assert-edit-user-permission"; @@ -20,7 +19,6 @@ export const zodSetPasswordUserAdminSchema = z.object({ }); export const setPasswordUserAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_edit" }, route: { method: "put", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/revoke-device.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/revoke-device.route.ts index e95bcb4707..81c60eafef 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/revoke-device.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/revoke-device.route.ts @@ -1,9 +1,8 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { revokeSessions } from "@/api/models/session-revoke"; import { findUserDeviceId } from "@/api/models/user-devices"; -import { CONFIG_PLUGIN } from "@/config"; import { assertCanEditAdminTarget } from "../lib/assert-edit-user-permission"; import { @@ -13,7 +12,6 @@ import { } from "../lib/target-user"; export const revokeUserDeviceAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_edit" }, route: { method: "delete", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/revoke-devices.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/revoke-devices.route.ts index c88e1e256b..304e851ea9 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/revoke-devices.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/revoke-devices.route.ts @@ -1,8 +1,7 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { revokeSessions } from "@/api/models/session-revoke"; -import { CONFIG_PLUGIN } from "@/config"; import { assertCanEditAdminTarget } from "../lib/assert-edit-user-permission"; import { @@ -13,7 +12,6 @@ import { } from "../lib/target-user"; export const revokeUserDevicesAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_edit" }, route: { method: "delete", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/show.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/show.route.ts index fcb2d06010..08b635e886 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/show.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/show.route.ts @@ -1,17 +1,15 @@ import { z } from "@hono/zod-openapi"; +import { buildRoute } from "@/api/builders"; import { isStaff } from "@/api/lib/check-staff-permission"; import { resolveUserRoles, userRoleSchema } from "@/api/lib/resolve-user-roles"; -import { buildRoute } from "@/api/lib/route"; import { resolveUserImagePolicy, zodUserImagePolicy, } from "@/api/lib/user-images"; import { UserModel } from "@/api/models/user"; -import { CONFIG_PLUGIN } from "@/config"; export const showUserAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/sso-disconnect.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/sso-disconnect.route.ts index d202886d31..506ba5f8ed 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/sso-disconnect.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/sso-disconnect.route.ts @@ -1,13 +1,12 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { SsoConnectionModel } from "@/api/models/sso-connection"; import { ssoConnectionFailure } from "@/api/modules/users/sso/connections/failure"; import { zodSsoConnectionErrorSchema, zodSsoProviderIdParam, } from "@/api/modules/users/sso/connections/schema"; -import { CONFIG_PLUGIN } from "@/config"; import { assertCanEditAdminTarget } from "../lib/assert-edit-user-permission"; import { @@ -17,7 +16,6 @@ import { } from "../lib/target-user"; export const disconnectUserSsoAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_edit" }, route: { method: "delete", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/sso-preferences.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/sso-preferences.route.ts index 802dddc788..c6e461914c 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/sso-preferences.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/sso-preferences.route.ts @@ -1,11 +1,10 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { SsoConnectionModel } from "@/api/models/sso-connection"; import { ssoConnectionFailure } from "@/api/modules/users/sso/connections/failure"; import { zodSsoPreferencesSchema } from "@/api/modules/users/sso/connections/routes/preferences.route"; import { zodSsoConnectionErrorSchema } from "@/api/modules/users/sso/connections/schema"; -import { CONFIG_PLUGIN } from "@/config"; import { assertCanEditAdminTarget } from "../lib/assert-edit-user-permission"; import { @@ -22,7 +21,6 @@ const ssoErrorResponse = (description: string) => ({ }); export const updateUserSsoPreferencesAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_edit" }, route: { method: "put", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/sso.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/sso.route.ts index 47b3f005a6..e6a835e42c 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/sso.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/sso.route.ts @@ -1,7 +1,6 @@ -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { SsoConnectionModel } from "@/api/models/sso-connection"; import { zodSsoConnectionsOverview } from "@/api/modules/users/sso/connections/schema"; -import { CONFIG_PLUGIN } from "@/config"; import { findTargetUserId, @@ -10,7 +9,6 @@ import { } from "../lib/target-user"; export const listUserSsoAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/timeline.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/timeline.route.ts index 219fd44e74..fe35b94a50 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/timeline.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/timeline.route.ts @@ -1,16 +1,14 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { zodSearchResultSchema } from "@/api/modules/search/routes/search.route"; import { searchTimeline, zodSearchTimelineQuery, zodSearchTimelineUserId, } from "@/api/modules/search/routes/timeline.route"; -import { CONFIG_PLUGIN } from "@/config"; export const timelineUserAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_view" }, route: { method: "get", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/update.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/update.route.ts index be20a741bb..18fb085814 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/update.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/update.route.ts @@ -1,13 +1,12 @@ import { z } from "@hono/zod-openapi"; import { and, eq, inArray, ne } from "drizzle-orm"; +import { buildRoute } from "@/api/builders"; import { isValidTimeZone } from "@/api/lib/notifications/digest-period"; -import { buildRoute } from "@/api/lib/route"; import { invalidateStaffPermissionsForUser } from "@/api/lib/staff-permission-cache"; import { matchesEmail } from "@/api/lib/user-email-lookup"; import { invalidateSessionCacheForUser } from "@/api/models/session-revoke"; import { SsoConnectionModel } from "@/api/models/sso-connection"; -import { CONFIG_PLUGIN } from "@/config"; import { core_languages } from "@/database/languages"; import { core_roles } from "@/database/roles"; import { core_users, core_users_secondary_roles } from "@/database/users"; @@ -100,7 +99,6 @@ export const zodUpdateUserAdminSchema = z }); export const updateUserAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_edit" }, route: { method: "patch", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/verify-email.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/verify-email.route.ts index 0f1d746b08..569a022817 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/verify-email.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/verify-email.route.ts @@ -1,15 +1,13 @@ import { z } from "@hono/zod-openapi"; import { eq } from "drizzle-orm"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { invalidateSessionCacheForUser } from "@/api/models/session-revoke"; -import { CONFIG_PLUGIN } from "@/config"; import { core_users, core_users_confirm_emails } from "@/database/users"; import { assertCanEditAdminTarget } from "../lib/assert-edit-user-permission"; export const verifyEmailUserAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "users", permission: "can_edit" }, route: { method: "post", diff --git a/packages/vitnode/src/api/modules/admin/users/users.admin.module.ts b/packages/vitnode/src/api/modules/admin/users/users.admin.module.ts index abc6635fa8..bc7771949d 100644 --- a/packages/vitnode/src/api/modules/admin/users/users.admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/users/users.admin.module.ts @@ -1,5 +1,4 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { createUserAdminRoute } from "./routes/create.route"; import { listUserDevicesAdminRoute } from "./routes/devices.route"; @@ -23,7 +22,6 @@ import { updateUserAdminRoute } from "./routes/update.route"; import { verifyEmailUserAdminRoute } from "./routes/verify-email.route"; export const usersAdminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "users", routes: [ listUsersAdminRoute, diff --git a/packages/vitnode/src/api/modules/ai/ai.module.ts b/packages/vitnode/src/api/modules/ai/ai.module.ts index 48aa8f7693..99ef101510 100644 --- a/packages/vitnode/src/api/modules/ai/ai.module.ts +++ b/packages/vitnode/src/api/modules/ai/ai.module.ts @@ -1,5 +1,4 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { aiQueueCron, altDetectCron } from "./cron/alt.cron"; import { aiMaintenanceCron } from "./cron/maintenance.cron"; @@ -12,7 +11,6 @@ import { aiUsageRoute } from "./routes/usage.route"; import { altGenerateQueueTask } from "./tasks/alt-generate.task"; export const aiModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "ai", routes: [ aiUsageRoute, diff --git a/packages/vitnode/src/api/modules/ai/routes/available.route.ts b/packages/vitnode/src/api/modules/ai/routes/available.route.ts index c9c8a08f54..578431b8bf 100644 --- a/packages/vitnode/src/api/modules/ai/routes/available.route.ts +++ b/packages/vitnode/src/api/modules/ai/routes/available.route.ts @@ -1,11 +1,9 @@ import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { availableAiActions } from "@/api/lib/ai/available-actions"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; export const availableAiActionsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: "The AI actions you may start.", diff --git a/packages/vitnode/src/api/modules/ai/routes/feedback.route.ts b/packages/vitnode/src/api/modules/ai/routes/feedback.route.ts index 81531107ca..0c7fe695c8 100644 --- a/packages/vitnode/src/api/modules/ai/routes/feedback.route.ts +++ b/packages/vitnode/src/api/modules/ai/routes/feedback.route.ts @@ -2,12 +2,10 @@ import { and, eq } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { core_ai_runs } from "@/database/ai"; export const aiFeedbackRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: "Record whether you accepted an AI suggestion.", diff --git a/packages/vitnode/src/api/modules/ai/routes/history.route.ts b/packages/vitnode/src/api/modules/ai/routes/history.route.ts index 57f4e4186a..3152237f7f 100644 --- a/packages/vitnode/src/api/modules/ai/routes/history.route.ts +++ b/packages/vitnode/src/api/modules/ai/routes/history.route.ts @@ -2,14 +2,12 @@ import { and, desc, eq, lt } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { core_ai_runs } from "@/database/ai"; const DEFAULT_PAGE_SIZE = 20; export const aiHistoryRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: "Your recent AI operations, newest first.", diff --git a/packages/vitnode/src/api/modules/ai/routes/run.route.ts b/packages/vitnode/src/api/modules/ai/routes/run.route.ts index 477c10af5e..3cfa03bb5f 100644 --- a/packages/vitnode/src/api/modules/ai/routes/run.route.ts +++ b/packages/vitnode/src/api/modules/ai/routes/run.route.ts @@ -1,13 +1,11 @@ +import { buildRoute } from "@/api/builders"; import { zodAiErrorBody, zodAiRunBody, zodAiRunResponse, } from "@/api/lib/ai/run-schemas"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; export const runAiRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: "Run a registered AI action for the signed-in user.", diff --git a/packages/vitnode/src/api/modules/ai/routes/stream.route.ts b/packages/vitnode/src/api/modules/ai/routes/stream.route.ts index b903d9e922..8f5a2e01cf 100644 --- a/packages/vitnode/src/api/modules/ai/routes/stream.route.ts +++ b/packages/vitnode/src/api/modules/ai/routes/stream.route.ts @@ -1,15 +1,13 @@ import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { zodAiRunBody } from "@/api/lib/ai/run-schemas"; import { AI_NDJSON_HEADERS, aiNdjsonStream, } from "@/api/lib/ai/stream-response"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; export const streamAiRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: "Stream a registered AI text action for the signed-in user.", @@ -43,7 +41,6 @@ export const streamAiRoute = buildRoute({ }); export const estimateAiRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: diff --git a/packages/vitnode/src/api/modules/ai/routes/usage.route.ts b/packages/vitnode/src/api/modules/ai/routes/usage.route.ts index 67a1cd9142..6498630713 100644 --- a/packages/vitnode/src/api/modules/ai/routes/usage.route.ts +++ b/packages/vitnode/src/api/modules/ai/routes/usage.route.ts @@ -1,9 +1,8 @@ import { HTTPException } from "hono/http-exception"; import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { loadUserAiUsage } from "@/api/lib/ai/usage-summary"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; export const zodAiUserUsage = z.object({ actions: z.array( @@ -37,7 +36,6 @@ export const zodAiUserUsage = z.object({ }); export const aiUsageRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: diff --git a/packages/vitnode/src/api/modules/content/content.module.ts b/packages/vitnode/src/api/modules/content/content.module.ts index f45f38cdbe..72ff467cb7 100644 --- a/packages/vitnode/src/api/modules/content/content.module.ts +++ b/packages/vitnode/src/api/modules/content/content.module.ts @@ -1,5 +1,4 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { contentLiveWebSocket } from "@/content/server/live/socket"; import { contentEditorialCleanupCron } from "./cron/content-editorial-cleanup.cron"; @@ -7,7 +6,6 @@ import { contentScheduleEffectsQueueTask } from "./tasks/content-schedule-effect import { contentScheduleQueueTask } from "./tasks/content-schedule.task"; export const contentModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "content", routes: [], cronJobs: [contentEditorialCleanupCron], diff --git a/packages/vitnode/src/api/modules/cron/cron.module.ts b/packages/vitnode/src/api/modules/cron/cron.module.ts index 8d88f0b57f..f07ca418e7 100644 --- a/packages/vitnode/src/api/modules/cron/cron.module.ts +++ b/packages/vitnode/src/api/modules/cron/cron.module.ts @@ -1,11 +1,9 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { cleanCron } from "./cron/clean.cron"; import { runCronRoute } from "./routes/cron.route"; export const cronModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "cron", routes: [runCronRoute], cronJobs: [cleanCron], diff --git a/packages/vitnode/src/api/modules/cron/routes/cron.route.ts b/packages/vitnode/src/api/modules/cron/routes/cron.route.ts index ca45666b14..e0b6c95276 100644 --- a/packages/vitnode/src/api/modules/cron/routes/cron.route.ts +++ b/packages/vitnode/src/api/modules/cron/routes/cron.route.ts @@ -1,9 +1,8 @@ import { eq } from "drizzle-orm"; import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { cronAuthMiddleware } from "@/api/middlewares/cron-auth.middleware"; -import { CONFIG_PLUGIN } from "@/config"; import { core_cron } from "@/database/cron"; import { getNextCronRunDate } from "@/lib/api/get-next-cron-run-date"; @@ -14,7 +13,6 @@ import { } from "../helpers/process-cron-jobs"; export const runCronRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: "Run cron job", diff --git a/packages/vitnode/src/api/modules/middleware/middleware.module.ts b/packages/vitnode/src/api/modules/middleware/middleware.module.ts index cd8240feb7..5775d2dab5 100644 --- a/packages/vitnode/src/api/modules/middleware/middleware.module.ts +++ b/packages/vitnode/src/api/modules/middleware/middleware.module.ts @@ -1,10 +1,8 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { routeMiddleware } from "./route"; export const middlewareModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "middleware", routes: [routeMiddleware], }); diff --git a/packages/vitnode/src/api/modules/middleware/route.ts b/packages/vitnode/src/api/modules/middleware/route.ts index 81bff2fd8c..d4eb627ace 100644 --- a/packages/vitnode/src/api/modules/middleware/route.ts +++ b/packages/vitnode/src/api/modules/middleware/route.ts @@ -1,13 +1,12 @@ import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { AI_MODEL_CAPABILITIES } from "@/api/lib/ai/capabilities"; -import { buildRoute } from "@/api/lib/route"; import { loadPublicNavigationMenus } from "@/api/modules/admin/navigation/lib/cache"; import { zodPublicNavigationItemSchema, zodPublicNavigationNodeSchema, } from "@/api/modules/admin/navigation/lib/schema"; -import { CONFIG_PLUGIN } from "@/config"; import { loadNavigationIcons, zodLucideIconSchema } from "./navigation-icons"; @@ -46,7 +45,6 @@ export const routeMiddlewareSchema = z.object({ }); export const routeMiddleware = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { path: "/", method: "get", diff --git a/packages/vitnode/src/api/modules/moderation/moderation.module.ts b/packages/vitnode/src/api/modules/moderation/moderation.module.ts index e8384dfefe..1a057f24be 100644 --- a/packages/vitnode/src/api/modules/moderation/moderation.module.ts +++ b/packages/vitnode/src/api/modules/moderation/moderation.module.ts @@ -1,5 +1,4 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { approvalFiltersRoute, @@ -27,7 +26,6 @@ import { } from "./routes/workflow.routes"; export const moderationModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "moderation", routes: [ createReportRoute, diff --git a/packages/vitnode/src/api/modules/moderation/routes/approvals.routes.ts b/packages/vitnode/src/api/modules/moderation/routes/approvals.routes.ts index a985238563..ff40165afa 100644 --- a/packages/vitnode/src/api/modules/moderation/routes/approvals.routes.ts +++ b/packages/vitnode/src/api/modules/moderation/routes/approvals.routes.ts @@ -3,10 +3,9 @@ import type { Context } from "hono"; import { z } from "@hono/zod-openapi"; import { HTTPException } from "hono/http-exception"; +import { buildRoute } from "@/api/builders"; import { requireModerator } from "@/api/lib/moderation/access"; import { zodPublicPerson } from "@/api/lib/public-people"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { CONTENT_SUBMISSION_DECISION_CODES, CONTENT_SUBMISSION_KINDS, @@ -125,7 +124,6 @@ const reviewableSubmission = async (c: Context, id: number) => { }; export const listApprovalsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", path: "/approvals", @@ -163,7 +161,6 @@ export const listApprovalsRoute = buildRoute({ }); export const approvalFiltersRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", path: "/approvals/filters", @@ -200,7 +197,6 @@ export const approvalFiltersRoute = buildRoute({ }); export const myApprovalsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", path: "/approvals/mine", @@ -231,7 +227,6 @@ export const myApprovalsRoute = buildRoute({ }); export const showApprovalRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", path: "/approvals/{id}", @@ -266,7 +261,6 @@ const decisionResponses = { }; export const approveApprovalRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", path: "/approvals/{id}/approve", @@ -296,7 +290,6 @@ export const approveApprovalRoute = buildRoute({ }); export const rejectApprovalRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", path: "/approvals/{id}/reject", diff --git a/packages/vitnode/src/api/modules/moderation/routes/create.route.ts b/packages/vitnode/src/api/modules/moderation/routes/create.route.ts index ac7771f92c..122c23eed5 100644 --- a/packages/vitnode/src/api/modules/moderation/routes/create.route.ts +++ b/packages/vitnode/src/api/modules/moderation/routes/create.route.ts @@ -2,11 +2,10 @@ import { z } from "@hono/zod-openapi"; import { and, eq } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; +import { buildRoute } from "@/api/builders"; import { isUniqueViolation } from "@/api/lib/db-errors"; import { findReportTarget } from "@/api/lib/moderation/registry"; import { sealReportSnapshot } from "@/api/lib/moderation/snapshot"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { core_moderation_reports } from "@/database/moderation"; import { MODERATION_LIMITS } from "@/lib/moderation/const"; @@ -50,7 +49,6 @@ const createBody = z ); export const createReportRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", path: "/reports", diff --git a/packages/vitnode/src/api/modules/moderation/routes/list.route.ts b/packages/vitnode/src/api/modules/moderation/routes/list.route.ts index 81e316c975..b0db5a1729 100644 --- a/packages/vitnode/src/api/modules/moderation/routes/list.route.ts +++ b/packages/vitnode/src/api/modules/moderation/routes/list.route.ts @@ -1,13 +1,12 @@ import { z } from "@hono/zod-openapi"; import { and, count, desc, eq, ilike, or } from "drizzle-orm"; +import { buildRoute } from "@/api/builders"; import { requireModerator, viewableReportsCondition, } from "@/api/lib/moderation/access"; import { findReportTarget } from "@/api/lib/moderation/registry"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { core_moderation_reports } from "@/database/moderation"; import { containsLikePattern } from "@/lib/api/like-pattern"; import { @@ -51,7 +50,6 @@ const searchCondition = (search: string) => { }; export const listReportsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", path: "/reports", diff --git a/packages/vitnode/src/api/modules/moderation/routes/notes.routes.ts b/packages/vitnode/src/api/modules/moderation/routes/notes.routes.ts index 3343520fa1..f73a11e8c0 100644 --- a/packages/vitnode/src/api/modules/moderation/routes/notes.routes.ts +++ b/packages/vitnode/src/api/modules/moderation/routes/notes.routes.ts @@ -1,9 +1,8 @@ import { z } from "@hono/zod-openapi"; import { eq } from "drizzle-orm"; +import { buildRoute } from "@/api/builders"; import { requireModerator } from "@/api/lib/moderation/access"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { core_moderation_report_events } from "@/database/moderation"; import { canChangeNote, loadReportNote } from "../lib/notes"; @@ -38,7 +37,6 @@ const noteResponses = { }; export const editReportNoteRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", path: "/reports/{id}/notes/{noteId}/edit", @@ -94,7 +92,6 @@ export const editReportNoteRoute = buildRoute({ }); export const deleteReportNoteRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", path: "/reports/{id}/notes/{noteId}/delete", diff --git a/packages/vitnode/src/api/modules/moderation/routes/show.route.ts b/packages/vitnode/src/api/modules/moderation/routes/show.route.ts index f36802a9c3..653e96b4d0 100644 --- a/packages/vitnode/src/api/modules/moderation/routes/show.route.ts +++ b/packages/vitnode/src/api/modules/moderation/routes/show.route.ts @@ -1,9 +1,8 @@ import { z } from "@hono/zod-openapi"; import { asc, eq } from "drizzle-orm"; +import { buildRoute } from "@/api/builders"; import { canOnReport, requireModerator } from "@/api/lib/moderation/access"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { core_moderation_report_events } from "@/database/moderation"; import { MODERATION_PERMISSIONS, @@ -88,7 +87,6 @@ const detailSchema = z.object({ }); export const showReportRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", path: "/reports/{id}", diff --git a/packages/vitnode/src/api/modules/moderation/routes/workflow.routes.ts b/packages/vitnode/src/api/modules/moderation/routes/workflow.routes.ts index 04c52de0a1..34f39426c8 100644 --- a/packages/vitnode/src/api/modules/moderation/routes/workflow.routes.ts +++ b/packages/vitnode/src/api/modules/moderation/routes/workflow.routes.ts @@ -1,10 +1,9 @@ import { z } from "@hono/zod-openapi"; import { and, eq } from "drizzle-orm"; +import { buildRoute } from "@/api/builders"; import { resolveStaffPermissions } from "@/api/lib/check-staff-permission"; import { canOnReport, requireModerator } from "@/api/lib/moderation/access"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { core_moderation_reports } from "@/database/moderation"; import { core_users } from "@/database/users"; import { MODERATION_PERMISSIONS } from "@/lib/moderation/const"; @@ -60,7 +59,6 @@ const body = (schema: T) => ({ }); export const claimReportRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", path: "/reports/{id}/claim", @@ -121,7 +119,6 @@ export const claimReportRoute = buildRoute({ }); export const assignReportRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", path: "/reports/{id}/assign", @@ -212,7 +209,6 @@ export const assignReportRoute = buildRoute({ }); export const noteReportRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", path: "/reports/{id}/notes", @@ -250,7 +246,6 @@ export const noteReportRoute = buildRoute({ }); export const resolveReportRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", path: "/reports/{id}/resolve", @@ -309,7 +304,6 @@ export const resolveReportRoute = buildRoute({ }); export const reopenReportRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", path: "/reports/{id}/reopen", @@ -382,7 +376,6 @@ export const reopenReportRoute = buildRoute({ }); export const reportActionRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", path: "/reports/{id}/actions/{action}", @@ -461,7 +454,6 @@ export const reportActionRoute = buildRoute({ }); export const reportAssigneesRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", path: "/reports/{id}/assignees", diff --git a/packages/vitnode/src/api/modules/notifications/notifications.module.ts b/packages/vitnode/src/api/modules/notifications/notifications.module.ts index ffbca9ec36..ee23654a24 100644 --- a/packages/vitnode/src/api/modules/notifications/notifications.module.ts +++ b/packages/vitnode/src/api/modules/notifications/notifications.module.ts @@ -1,6 +1,5 @@ -import { buildModule } from "@/api/lib/module"; +import { buildModule } from "@/api/builders"; import { coreNotificationTypes } from "@/api/lib/notifications/core-types"; -import { CONFIG_PLUGIN } from "@/config"; import { listNotificationsRoute } from "./routes/list.route"; import { @@ -23,7 +22,6 @@ import { } from "./tasks/notification-tasks"; export const notificationsModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "notifications", routes: [ listNotificationsRoute, diff --git a/packages/vitnode/src/api/modules/notifications/routes/list.route.ts b/packages/vitnode/src/api/modules/notifications/routes/list.route.ts index 80d18d6ad3..91820ac4a4 100644 --- a/packages/vitnode/src/api/modules/notifications/routes/list.route.ts +++ b/packages/vitnode/src/api/modules/notifications/routes/list.route.ts @@ -1,8 +1,7 @@ import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { listNotifications } from "@/api/models/notifications/inbox"; -import { CONFIG_PLUGIN } from "@/config"; import { requireNotificationUser, @@ -11,7 +10,6 @@ import { } from "../schema"; export const listNotificationsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: diff --git a/packages/vitnode/src/api/modules/notifications/routes/mark.route.ts b/packages/vitnode/src/api/modules/notifications/routes/mark.route.ts index c0bd5c0a03..84e5d67674 100644 --- a/packages/vitnode/src/api/modules/notifications/routes/mark.route.ts +++ b/packages/vitnode/src/api/modules/notifications/routes/mark.route.ts @@ -1,13 +1,12 @@ import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { archiveNotification, markAllNotificationsRead, markNotificationRead, markNotificationUnread, } from "@/api/models/notifications/inbox"; -import { CONFIG_PLUGIN } from "@/config"; import { requireNotificationUser, @@ -26,7 +25,6 @@ const stateResponse = { } as const; export const markNotificationReadRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: @@ -65,7 +63,6 @@ export const markNotificationReadRoute = buildRoute({ }); export const markNotificationUnreadRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: "Mark one notification unread again.", @@ -84,7 +81,6 @@ export const markNotificationUnreadRoute = buildRoute({ }); export const archiveNotificationRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: @@ -104,7 +100,6 @@ export const archiveNotificationRoute = buildRoute({ }); export const markAllNotificationsReadRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: diff --git a/packages/vitnode/src/api/modules/notifications/routes/preferences.route.ts b/packages/vitnode/src/api/modules/notifications/routes/preferences.route.ts index 1370a4f3aa..91895eca65 100644 --- a/packages/vitnode/src/api/modules/notifications/routes/preferences.route.ts +++ b/packages/vitnode/src/api/modules/notifications/routes/preferences.route.ts @@ -1,11 +1,10 @@ import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { getNotificationPreferences, updateNotificationPreferences, } from "@/api/models/notifications/preferences"; -import { CONFIG_PLUGIN } from "@/config"; import { requireNotificationUser, @@ -38,7 +37,6 @@ export const zodNotificationPreferences = z.object({ }); export const getNotificationPreferencesRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: @@ -78,7 +76,6 @@ export const zodUpdateNotificationPreferences = z.object({ }); export const updateNotificationPreferencesRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "put", description: diff --git a/packages/vitnode/src/api/modules/notifications/routes/state.route.ts b/packages/vitnode/src/api/modules/notifications/routes/state.route.ts index 07bb08077a..0bb52b3aab 100644 --- a/packages/vitnode/src/api/modules/notifications/routes/state.route.ts +++ b/packages/vitnode/src/api/modules/notifications/routes/state.route.ts @@ -1,6 +1,5 @@ -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { getNotificationState } from "@/api/models/notifications/inbox"; -import { CONFIG_PLUGIN } from "@/config"; import { requireNotificationUser, @@ -9,7 +8,6 @@ import { } from "../schema"; export const notificationStateRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: diff --git a/packages/vitnode/src/api/modules/pages/pages.module.ts b/packages/vitnode/src/api/modules/pages/pages.module.ts index 44caf873a0..9d81747a14 100644 --- a/packages/vitnode/src/api/modules/pages/pages.module.ts +++ b/packages/vitnode/src/api/modules/pages/pages.module.ts @@ -1,10 +1,9 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; +import { manifest } from "@/manifest"; import { buildPageLayoutRoutes } from "./page-layout-routes"; export const pagesModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "pages", - routes: buildPageLayoutRoutes({ pluginId: CONFIG_PLUGIN.pluginId }), + routes: buildPageLayoutRoutes({ pluginId: manifest.id }), }); diff --git a/packages/vitnode/src/api/modules/queue/queue.module.ts b/packages/vitnode/src/api/modules/queue/queue.module.ts index 964f4cecb7..5819f435e5 100644 --- a/packages/vitnode/src/api/modules/queue/queue.module.ts +++ b/packages/vitnode/src/api/modules/queue/queue.module.ts @@ -1,11 +1,9 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { processQueueCron } from "./cron/process-queue.cron"; import { sendEmailQueueTask } from "./tasks/send-email.task"; export const queueModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "queue", routes: [], cronJobs: [processQueueCron], diff --git a/packages/vitnode/src/api/modules/search/routes/authors.route.ts b/packages/vitnode/src/api/modules/search/routes/authors.route.ts index 580f747765..c8d0a73bab 100644 --- a/packages/vitnode/src/api/modules/search/routes/authors.route.ts +++ b/packages/vitnode/src/api/modules/search/routes/authors.route.ts @@ -1,14 +1,12 @@ import { z } from "@hono/zod-openapi"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; -import { buildRoute } from "../../../lib/route"; import { zodSearchHitAuthor } from "./search.route"; export const SEARCH_AUTHORS_LIMIT = 10; export const authorsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: diff --git a/packages/vitnode/src/api/modules/search/routes/facets.route.ts b/packages/vitnode/src/api/modules/search/routes/facets.route.ts index 22fc22915e..fb00b5a6d9 100644 --- a/packages/vitnode/src/api/modules/search/routes/facets.route.ts +++ b/packages/vitnode/src/api/modules/search/routes/facets.route.ts @@ -1,9 +1,8 @@ import { z } from "@hono/zod-openapi"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { contentSearchHiddenAccess } from "@/content/server/hidden-access"; -import { buildRoute } from "../../../lib/route"; import { dateOrUndefined, positiveIntListOrUndefined, @@ -11,7 +10,6 @@ import { } from "./search.route"; export const facetsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: diff --git a/packages/vitnode/src/api/modules/search/routes/popular.route.ts b/packages/vitnode/src/api/modules/search/routes/popular.route.ts index ffd749c9d3..fc185b15d3 100644 --- a/packages/vitnode/src/api/modules/search/routes/popular.route.ts +++ b/packages/vitnode/src/api/modules/search/routes/popular.route.ts @@ -1,12 +1,10 @@ import { z } from "@hono/zod-openapi"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; -import { buildRoute } from "../../../lib/route"; import { searchSuggestionsLimit, zodSearchSuggestion } from "./suggest.route"; export const popularRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: diff --git a/packages/vitnode/src/api/modules/search/routes/popularity.route.ts b/packages/vitnode/src/api/modules/search/routes/popularity.route.ts index 605fe0a4f3..62d4c41e10 100644 --- a/packages/vitnode/src/api/modules/search/routes/popularity.route.ts +++ b/packages/vitnode/src/api/modules/search/routes/popularity.route.ts @@ -3,12 +3,10 @@ import type { Context } from "hono"; import { z } from "@hono/zod-openapi"; import { createHash } from "node:crypto"; +import { buildRoute } from "@/api/builders"; import { resolveClientIp } from "@/api/lib/client-ip"; -import { CONFIG_PLUGIN } from "@/config"; import { CONTENT_ID_KEY_LENGTH } from "@/content/const"; -import { buildRoute } from "../../../lib/route"; - const SEARCH_POPULARITY_REPEAT_WINDOW_SECONDS = 30 * 60; const viewerKey = (c: Context): string => { @@ -19,7 +17,6 @@ const viewerKey = (c: Context): string => { }; export const popularityRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: diff --git a/packages/vitnode/src/api/modules/search/routes/search.route.ts b/packages/vitnode/src/api/modules/search/routes/search.route.ts index d0903ced24..79cca610d3 100644 --- a/packages/vitnode/src/api/modules/search/routes/search.route.ts +++ b/packages/vitnode/src/api/modules/search/routes/search.route.ts @@ -1,9 +1,8 @@ import { z } from "@hono/zod-openapi"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { contentSearchHiddenAccess } from "@/content/server/hidden-access"; -import { buildRoute } from "../../../lib/route"; import { zodPaginationQuery } from "../../../lib/with-pagination"; const zodSearchPageInfo = z.object({ @@ -113,7 +112,6 @@ export const zodSearchResultSchema = z.object({ }); export const searchRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: "Search and browse indexed community content.", diff --git a/packages/vitnode/src/api/modules/search/routes/suggest.route.ts b/packages/vitnode/src/api/modules/search/routes/suggest.route.ts index c6aadba899..cb7769600f 100644 --- a/packages/vitnode/src/api/modules/search/routes/suggest.route.ts +++ b/packages/vitnode/src/api/modules/search/routes/suggest.route.ts @@ -1,10 +1,9 @@ import { z } from "@hono/zod-openapi"; +import { buildRoute } from "@/api/builders"; import { MAX_SEARCH_SUGGESTIONS } from "@/api/lib/search-suggestions"; -import { CONFIG_PLUGIN } from "@/config"; import { contentSearchHiddenAccess } from "@/content/server/hidden-access"; -import { buildRoute } from "../../../lib/route"; import { positiveIntOrUndefined } from "./search.route"; const DEFAULT_SEARCH_SUGGESTIONS = 6; @@ -34,7 +33,6 @@ export const searchSuggestionsLimit = (first: string | undefined): number => ); export const suggestRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: diff --git a/packages/vitnode/src/api/modules/search/routes/timeline.route.ts b/packages/vitnode/src/api/modules/search/routes/timeline.route.ts index 8a0cd5211b..1b6af0eae4 100644 --- a/packages/vitnode/src/api/modules/search/routes/timeline.route.ts +++ b/packages/vitnode/src/api/modules/search/routes/timeline.route.ts @@ -4,10 +4,9 @@ import { z } from "@hono/zod-openapi"; import type { SearchHiddenAccess } from "@/api/models/search"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; import { contentSearchHiddenAccess } from "@/content/server/hidden-access"; -import { buildRoute } from "../../../lib/route"; import { zodPaginationQuery } from "../../../lib/with-pagination"; import { positiveIntOrUndefined, zodSearchResultSchema } from "./search.route"; @@ -45,7 +44,6 @@ export const searchTimeline = async ( }); export const timelineRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: diff --git a/packages/vitnode/src/api/modules/search/search.module.ts b/packages/vitnode/src/api/modules/search/search.module.ts index 63b8c4dd44..1d136f989c 100644 --- a/packages/vitnode/src/api/modules/search/search.module.ts +++ b/packages/vitnode/src/api/modules/search/search.module.ts @@ -1,5 +1,4 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { authorsRoute } from "./routes/authors.route"; import { facetsRoute } from "./routes/facets.route"; @@ -11,7 +10,6 @@ import { timelineRoute } from "./routes/timeline.route"; import { rebuildSearchIndexTask } from "./tasks/rebuild-index.task"; export const searchModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "search", routes: [ searchRoute, diff --git a/packages/vitnode/src/api/modules/users/files/files.module.ts b/packages/vitnode/src/api/modules/users/files/files.module.ts index ce1724957b..98b4e6f96e 100644 --- a/packages/vitnode/src/api/modules/users/files/files.module.ts +++ b/packages/vitnode/src/api/modules/users/files/files.module.ts @@ -1,12 +1,10 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { deleteUserFileRoute } from "./routes/delete.route"; import { downloadUserFileRoute } from "./routes/download.route"; import { listUserFilesRoute } from "./routes/list.route"; export const userFilesModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "files", routes: [listUserFilesRoute, downloadUserFileRoute, deleteUserFileRoute], }); diff --git a/packages/vitnode/src/api/modules/users/files/routes/delete.route.ts b/packages/vitnode/src/api/modules/users/files/routes/delete.route.ts index 771cf83eb1..dcf3be6d02 100644 --- a/packages/vitnode/src/api/modules/users/files/routes/delete.route.ts +++ b/packages/vitnode/src/api/modules/users/files/routes/delete.route.ts @@ -1,11 +1,9 @@ import { z } from "@hono/zod-openapi"; import { HTTPException } from "hono/http-exception"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildRoute } from "@/api/builders"; export const deleteUserFileRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "delete", description: "Delete one of the current user's files.", diff --git a/packages/vitnode/src/api/modules/users/files/routes/download.route.ts b/packages/vitnode/src/api/modules/users/files/routes/download.route.ts index 53cb43816e..c832a6fa7a 100644 --- a/packages/vitnode/src/api/modules/users/files/routes/download.route.ts +++ b/packages/vitnode/src/api/modules/users/files/routes/download.route.ts @@ -2,13 +2,11 @@ import { z } from "@hono/zod-openapi"; import { and, eq } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; +import { buildRoute } from "@/api/builders"; import { attachStoredFile } from "@/api/lib/file-download"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; import { core_files } from "@/database/files"; export const downloadUserFileRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: diff --git a/packages/vitnode/src/api/modules/users/files/routes/list.route.ts b/packages/vitnode/src/api/modules/users/files/routes/list.route.ts index a16cfb5942..eff90524bc 100644 --- a/packages/vitnode/src/api/modules/users/files/routes/list.route.ts +++ b/packages/vitnode/src/api/modules/users/files/routes/list.route.ts @@ -2,19 +2,17 @@ import { and, eq } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; import z from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { notAttachedAsUserImage } from "@/api/lib/user-images"; import { withPagination, zodPaginationPageInfo, zodPaginationQuery, } from "@/api/lib/with-pagination"; -import { CONFIG_PLUGIN } from "@/config"; import { core_files } from "@/database/files"; import { parseImageDimensions } from "@/lib/api/upload"; export const listUserFilesRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: "List files uploaded by the current user.", diff --git a/packages/vitnode/src/api/modules/users/images/images.module.ts b/packages/vitnode/src/api/modules/users/images/images.module.ts index 70564bc045..23f53c3903 100644 --- a/packages/vitnode/src/api/modules/users/images/images.module.ts +++ b/packages/vitnode/src/api/modules/users/images/images.module.ts @@ -1,12 +1,10 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { deleteUserImageRoute } from "./routes/delete.route"; import { userImagePolicyRoute } from "./routes/policy.route"; import { uploadUserImageRoute } from "./routes/upload.route"; export const userImagesModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "images", routes: [userImagePolicyRoute, uploadUserImageRoute, deleteUserImageRoute], }); diff --git a/packages/vitnode/src/api/modules/users/images/routes/delete.route.ts b/packages/vitnode/src/api/modules/users/images/routes/delete.route.ts index f942e2cc99..f0057e8b82 100644 --- a/packages/vitnode/src/api/modules/users/images/routes/delete.route.ts +++ b/packages/vitnode/src/api/modules/users/images/routes/delete.route.ts @@ -1,13 +1,11 @@ import { z } from "@hono/zod-openapi"; import { HTTPException } from "hono/http-exception"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { removeUserImage, zodUserImageKind } from "@/api/lib/user-images"; import { SsoConnectionModel } from "@/api/models/sso-connection"; -import { CONFIG_PLUGIN } from "@/config"; export const deleteUserImageRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "delete", description: "Remove the signed-in user's avatar or cover.", diff --git a/packages/vitnode/src/api/modules/users/images/routes/policy.route.ts b/packages/vitnode/src/api/modules/users/images/routes/policy.route.ts index 430b71c231..9915911e78 100644 --- a/packages/vitnode/src/api/modules/users/images/routes/policy.route.ts +++ b/packages/vitnode/src/api/modules/users/images/routes/policy.route.ts @@ -1,14 +1,12 @@ import { HTTPException } from "hono/http-exception"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { resolveUserImagePolicy, zodUserImagePolicy, } from "@/api/lib/user-images"; -import { CONFIG_PLUGIN } from "@/config"; export const userImagePolicyRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: diff --git a/packages/vitnode/src/api/modules/users/images/routes/upload.route.ts b/packages/vitnode/src/api/modules/users/images/routes/upload.route.ts index abf974cec4..f899ed74c7 100644 --- a/packages/vitnode/src/api/modules/users/images/routes/upload.route.ts +++ b/packages/vitnode/src/api/modules/users/images/routes/upload.route.ts @@ -1,17 +1,15 @@ import { z } from "@hono/zod-openapi"; import { HTTPException } from "hono/http-exception"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { resolveUserImagePolicy, setUserImage, zodUserImageKind, } from "@/api/lib/user-images"; import { SsoConnectionModel } from "@/api/models/sso-connection"; -import { CONFIG_PLUGIN } from "@/config"; export const uploadUserImageRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: diff --git a/packages/vitnode/src/api/modules/users/passkeys/passkeys.module.ts b/packages/vitnode/src/api/modules/users/passkeys/passkeys.module.ts index 83e5197ef3..e15276d25a 100644 --- a/packages/vitnode/src/api/modules/users/passkeys/passkeys.module.ts +++ b/packages/vitnode/src/api/modules/users/passkeys/passkeys.module.ts @@ -1,5 +1,4 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { passkeyAdminSignInOptionsRoute } from "./routes/admin-sign-in-options.route"; import { passkeyAdminSignInVerifyRoute } from "./routes/admin-sign-in-verify.route"; @@ -12,7 +11,6 @@ import { passkeyRegistrationVerifyRoute } from "./routes/registration-verify.rou import { renamePasskeyRoute } from "./routes/rename.route"; export const passkeysUserModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "passkeys", routes: [ listPasskeysRoute, diff --git a/packages/vitnode/src/api/modules/users/passkeys/routes/admin-sign-in-options.route.ts b/packages/vitnode/src/api/modules/users/passkeys/routes/admin-sign-in-options.route.ts index f8c7919cd2..cc750d7204 100644 --- a/packages/vitnode/src/api/modules/users/passkeys/routes/admin-sign-in-options.route.ts +++ b/packages/vitnode/src/api/modules/users/passkeys/routes/admin-sign-in-options.route.ts @@ -1,6 +1,5 @@ -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { PasskeyModel } from "@/api/models/passkey"; -import { CONFIG_PLUGIN } from "@/config"; import { passkeyFailure } from "../failure"; import { @@ -9,7 +8,6 @@ import { } from "../schema"; export const passkeyAdminSignInOptionsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: diff --git a/packages/vitnode/src/api/modules/users/passkeys/routes/admin-sign-in-verify.route.ts b/packages/vitnode/src/api/modules/users/passkeys/routes/admin-sign-in-verify.route.ts index 587264729e..53b4231976 100644 --- a/packages/vitnode/src/api/modules/users/passkeys/routes/admin-sign-in-verify.route.ts +++ b/packages/vitnode/src/api/modules/users/passkeys/routes/admin-sign-in-verify.route.ts @@ -1,10 +1,9 @@ import { z } from "@hono/zod-openapi"; +import { buildRoute } from "@/api/builders"; import { isStaff } from "@/api/lib/check-staff-permission"; -import { buildRoute } from "@/api/lib/route"; import { PasskeyError, PasskeyModel } from "@/api/models/passkey"; import { SessionAdminModel } from "@/api/models/session-admin"; -import { CONFIG_PLUGIN } from "@/config"; import { passkeyFailure } from "../failure"; import { @@ -13,7 +12,6 @@ import { } from "../schema"; export const passkeyAdminSignInVerifyRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: diff --git a/packages/vitnode/src/api/modules/users/passkeys/routes/authentication-options.route.ts b/packages/vitnode/src/api/modules/users/passkeys/routes/authentication-options.route.ts index c5c3062677..ef9911e07c 100644 --- a/packages/vitnode/src/api/modules/users/passkeys/routes/authentication-options.route.ts +++ b/packages/vitnode/src/api/modules/users/passkeys/routes/authentication-options.route.ts @@ -1,6 +1,5 @@ -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { PasskeyModel } from "@/api/models/passkey"; -import { CONFIG_PLUGIN } from "@/config"; import { passkeyFailure } from "../failure"; import { @@ -9,7 +8,6 @@ import { } from "../schema"; export const passkeyAuthenticationOptionsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: diff --git a/packages/vitnode/src/api/modules/users/passkeys/routes/authentication-verify.route.ts b/packages/vitnode/src/api/modules/users/passkeys/routes/authentication-verify.route.ts index 5f776940b6..84d7d9924b 100644 --- a/packages/vitnode/src/api/modules/users/passkeys/routes/authentication-verify.route.ts +++ b/packages/vitnode/src/api/modules/users/passkeys/routes/authentication-verify.route.ts @@ -1,14 +1,13 @@ import { z } from "@hono/zod-openapi"; import { eq } from "drizzle-orm"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { EMAIL_NOT_VERIFIED, mustVerifyEmail, } from "@/api/models/email-verification"; import { PasskeyError, PasskeyModel } from "@/api/models/passkey"; import { SessionModel } from "@/api/models/session"; -import { CONFIG_PLUGIN } from "@/config"; import { core_users } from "@/database/users"; import { passkeyFailure } from "../failure"; @@ -18,7 +17,6 @@ import { } from "../schema"; export const passkeyAuthenticationVerifyRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: diff --git a/packages/vitnode/src/api/modules/users/passkeys/routes/delete.route.ts b/packages/vitnode/src/api/modules/users/passkeys/routes/delete.route.ts index c6ae66783c..e2c10f0dee 100644 --- a/packages/vitnode/src/api/modules/users/passkeys/routes/delete.route.ts +++ b/packages/vitnode/src/api/modules/users/passkeys/routes/delete.route.ts @@ -1,14 +1,12 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { PasskeyModel } from "@/api/models/passkey"; -import { CONFIG_PLUGIN } from "@/config"; import { passkeyFailure, requireSignedInUser } from "../failure"; import { PASSKEY_ERROR_RESPONSES, zodPasskeyIdParam } from "../schema"; export const deletePasskeyRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "delete", description: diff --git a/packages/vitnode/src/api/modules/users/passkeys/routes/list.route.ts b/packages/vitnode/src/api/modules/users/passkeys/routes/list.route.ts index 2138d81a1c..8fe3054528 100644 --- a/packages/vitnode/src/api/modules/users/passkeys/routes/list.route.ts +++ b/packages/vitnode/src/api/modules/users/passkeys/routes/list.route.ts @@ -1,14 +1,12 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { PasskeyModel } from "@/api/models/passkey"; -import { CONFIG_PLUGIN } from "@/config"; import { passkeyFailure, requireSignedInUser } from "../failure"; import { PASSKEY_ERROR_RESPONSES, zodPasskeySchema } from "../schema"; export const listPasskeysRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: "List the signed-in user's passkeys.", diff --git a/packages/vitnode/src/api/modules/users/passkeys/routes/registration-options.route.ts b/packages/vitnode/src/api/modules/users/passkeys/routes/registration-options.route.ts index 7551cd82c5..889c7f7ddb 100644 --- a/packages/vitnode/src/api/modules/users/passkeys/routes/registration-options.route.ts +++ b/packages/vitnode/src/api/modules/users/passkeys/routes/registration-options.route.ts @@ -1,6 +1,5 @@ -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { PasskeyModel } from "@/api/models/passkey"; -import { CONFIG_PLUGIN } from "@/config"; import { passkeyFailure, requireSignedInUser } from "../failure"; import { @@ -9,7 +8,6 @@ import { } from "../schema"; export const passkeyRegistrationOptionsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: diff --git a/packages/vitnode/src/api/modules/users/passkeys/routes/registration-verify.route.ts b/packages/vitnode/src/api/modules/users/passkeys/routes/registration-verify.route.ts index c07a3beb06..dbba2c5b56 100644 --- a/packages/vitnode/src/api/modules/users/passkeys/routes/registration-verify.route.ts +++ b/packages/vitnode/src/api/modules/users/passkeys/routes/registration-verify.route.ts @@ -1,8 +1,7 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { PasskeyModel } from "@/api/models/passkey"; -import { CONFIG_PLUGIN } from "@/config"; import { passkeyFailure, requireSignedInUser } from "../failure"; import { @@ -13,7 +12,6 @@ import { } from "../schema"; export const passkeyRegistrationVerifyRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: diff --git a/packages/vitnode/src/api/modules/users/passkeys/routes/rename.route.ts b/packages/vitnode/src/api/modules/users/passkeys/routes/rename.route.ts index 07e8f416e3..2ff72d9dca 100644 --- a/packages/vitnode/src/api/modules/users/passkeys/routes/rename.route.ts +++ b/packages/vitnode/src/api/modules/users/passkeys/routes/rename.route.ts @@ -1,8 +1,7 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { PasskeyModel } from "@/api/models/passkey"; -import { CONFIG_PLUGIN } from "@/config"; import { passkeyFailure, requireSignedInUser } from "../failure"; import { @@ -13,7 +12,6 @@ import { } from "../schema"; export const renamePasskeyRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "patch", description: "Rename one of the signed-in user's passkeys.", diff --git a/packages/vitnode/src/api/modules/users/routes/change-password.route.ts b/packages/vitnode/src/api/modules/users/routes/change-password.route.ts index 7a5d6f283b..cab04e6610 100644 --- a/packages/vitnode/src/api/modules/users/routes/change-password.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/change-password.route.ts @@ -2,11 +2,10 @@ import { and, eq, gt } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { assertPasswordSignInEnabled } from "@/api/lib/password-sign-in"; -import { buildRoute } from "@/api/lib/route"; import { ForgotPasswordTokenModel, PasswordModel } from "@/api/models/password"; import { revokeSessions } from "@/api/models/session-revoke"; -import { CONFIG_PLUGIN } from "@/config"; import { core_users, core_users_forgot_password } from "@/database/users"; import { USER_PASSWORD_MAX_LENGTH } from "../credential-limits"; @@ -20,7 +19,6 @@ export const zodChangePasswordSchema = z.object({ }); export const changePasswordRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: "Change user password", diff --git a/packages/vitnode/src/api/modules/users/routes/devices.route.ts b/packages/vitnode/src/api/modules/users/routes/devices.route.ts index d5aab7f277..9dcbf2c6fe 100644 --- a/packages/vitnode/src/api/modules/users/routes/devices.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/devices.route.ts @@ -2,15 +2,13 @@ import { getCookie } from "hono/cookie"; import { HTTPException } from "hono/http-exception"; import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { listUserDevices, zodUserDeviceSchema, } from "@/api/models/user-devices"; -import { CONFIG_PLUGIN } from "@/config"; export const listDevicesRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: diff --git a/packages/vitnode/src/api/modules/users/routes/me-policy.route.ts b/packages/vitnode/src/api/modules/users/routes/me-policy.route.ts index bf98da90fd..129779a78c 100644 --- a/packages/vitnode/src/api/modules/users/routes/me-policy.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/me-policy.route.ts @@ -1,14 +1,12 @@ import { HTTPException } from "hono/http-exception"; +import { buildRoute } from "@/api/builders"; import { resolvePersonalInfoPolicy, zodPersonalInfoPolicy, } from "@/api/lib/personal-info-policy"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; export const mePolicyRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: diff --git a/packages/vitnode/src/api/modules/users/routes/permissions.route.ts b/packages/vitnode/src/api/modules/users/routes/permissions.route.ts index 0e5e5700f9..47ff00a0a5 100644 --- a/packages/vitnode/src/api/modules/users/routes/permissions.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/permissions.route.ts @@ -1,11 +1,9 @@ import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { resolveStaffPermissions } from "@/api/lib/check-staff-permission"; -import { buildRoute } from "@/api/lib/route"; -import { CONFIG_PLUGIN } from "@/config"; export const permissionsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: diff --git a/packages/vitnode/src/api/modules/users/routes/profile.route.ts b/packages/vitnode/src/api/modules/users/routes/profile.route.ts index f27e50b029..6b2168503b 100644 --- a/packages/vitnode/src/api/modules/users/routes/profile.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/profile.route.ts @@ -1,13 +1,11 @@ import { z } from "@hono/zod-openapi"; +import { buildRoute } from "@/api/builders"; import { resolveUserRoles, userRoleSchema } from "@/api/lib/resolve-user-roles"; -import { buildRoute } from "@/api/lib/route"; import { UserModel } from "@/api/models/user"; -import { CONFIG_PLUGIN } from "@/config"; import { displayNameOf } from "@/lib/user-personal-information"; export const profileRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: "Public profile of a user, looked up by their URL name", diff --git a/packages/vitnode/src/api/modules/users/routes/resend-verification.route.ts b/packages/vitnode/src/api/modules/users/routes/resend-verification.route.ts index db3e0878c1..6e43b1476c 100644 --- a/packages/vitnode/src/api/modules/users/routes/resend-verification.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/resend-verification.route.ts @@ -1,19 +1,17 @@ import { HTTPException } from "hono/http-exception"; import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { matchesEmail, pickAccountForEmail } from "@/api/lib/user-email-lookup"; import { EmailVerificationModel, isEmailVerificationRequired, } from "@/api/models/email-verification"; -import { CONFIG_PLUGIN } from "@/config"; import { core_users } from "@/database/users"; import { USER_EMAIL_MAX_LENGTH } from "../credential-limits"; export const resendVerificationRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: diff --git a/packages/vitnode/src/api/modules/users/routes/reset-passowrd.route.ts b/packages/vitnode/src/api/modules/users/routes/reset-passowrd.route.ts index c3636fd021..c31c971e42 100644 --- a/packages/vitnode/src/api/modules/users/routes/reset-passowrd.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/reset-passowrd.route.ts @@ -3,11 +3,10 @@ import { HTTPException } from "hono/http-exception"; import { createTranslator } from "use-intl"; import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { assertPasswordSignInEnabled } from "@/api/lib/password-sign-in"; -import { buildRoute } from "@/api/lib/route"; import { matchesEmail, pickAccountForEmail } from "@/api/lib/user-email-lookup"; import { ForgotPasswordTokenModel } from "@/api/models/password"; -import { CONFIG_PLUGIN } from "@/config"; import { core_users, core_users_forgot_password } from "@/database/users"; import ResetPasswordEmailTemplate from "@/emails/reset-password"; import { CONFIG } from "@/lib/config"; @@ -15,7 +14,6 @@ import { CONFIG } from "@/lib/config"; import { USER_EMAIL_MAX_LENGTH } from "../credential-limits"; export const resetPasswordRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: "Request a password reset", diff --git a/packages/vitnode/src/api/modules/users/routes/revoke-device.route.ts b/packages/vitnode/src/api/modules/users/routes/revoke-device.route.ts index 9ae8f0ac1e..e4c9666b2c 100644 --- a/packages/vitnode/src/api/modules/users/routes/revoke-device.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/revoke-device.route.ts @@ -2,13 +2,11 @@ import { z } from "@hono/zod-openapi"; import { getCookie } from "hono/cookie"; import { HTTPException } from "hono/http-exception"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { revokeSessions } from "@/api/models/session-revoke"; import { findUserDeviceId } from "@/api/models/user-devices"; -import { CONFIG_PLUGIN } from "@/config"; export const revokeDeviceRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "delete", description: "Sign out one of the current user's devices.", diff --git a/packages/vitnode/src/api/modules/users/routes/session.route.ts b/packages/vitnode/src/api/modules/users/routes/session.route.ts index 89985c2290..bb8660fbfd 100644 --- a/packages/vitnode/src/api/modules/users/routes/session.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/session.route.ts @@ -1,13 +1,11 @@ import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { getStaffFlags } from "@/api/lib/check-staff-permission"; -import { buildRoute } from "@/api/lib/route"; import { getNotificationState } from "@/api/models/notifications/inbox"; import { zodNotificationState } from "@/api/modules/notifications/schema"; -import { CONFIG_PLUGIN } from "@/config"; export const sessionRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: "Verify session", diff --git a/packages/vitnode/src/api/modules/users/routes/sign-in.route.ts b/packages/vitnode/src/api/modules/users/routes/sign-in.route.ts index b07c19ccee..dee8e599bd 100644 --- a/packages/vitnode/src/api/modules/users/routes/sign-in.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/sign-in.route.ts @@ -1,7 +1,7 @@ import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { assertPasswordSignInEnabled } from "@/api/lib/password-sign-in"; -import { buildRoute } from "@/api/lib/route"; import { EMAIL_NOT_VERIFIED, mustVerifyEmail, @@ -9,7 +9,6 @@ import { import { SessionModel } from "@/api/models/session"; import { SessionAdminModel } from "@/api/models/session-admin"; import { UserModel } from "@/api/models/user"; -import { CONFIG_PLUGIN } from "@/config"; import { USER_EMAIL_MAX_LENGTH, @@ -29,7 +28,6 @@ export const zodSignInSchema = z.object({ }); export const signInRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: "Sign in with email and password", diff --git a/packages/vitnode/src/api/modules/users/routes/sign-out.route.ts b/packages/vitnode/src/api/modules/users/routes/sign-out.route.ts index 9ab991c8b6..0516cf315b 100644 --- a/packages/vitnode/src/api/modules/users/routes/sign-out.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/sign-out.route.ts @@ -1,12 +1,10 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { SessionModel } from "@/api/models/session"; import { SessionAdminModel } from "@/api/models/session-admin"; -import { CONFIG_PLUGIN } from "@/config"; export const signOutRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "delete", description: "Sign out the current admin", diff --git a/packages/vitnode/src/api/modules/users/routes/sign-up.route.ts b/packages/vitnode/src/api/modules/users/routes/sign-up.route.ts index 051740dd8a..ffb230f0c3 100644 --- a/packages/vitnode/src/api/modules/users/routes/sign-up.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/sign-up.route.ts @@ -1,12 +1,11 @@ import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { describeError } from "@/api/lib/error-details"; import { assertPasswordSignInEnabled } from "@/api/lib/password-sign-in"; -import { buildRoute } from "@/api/lib/route"; import { EmailVerificationModel } from "@/api/models/email-verification"; import { PasswordModel } from "@/api/models/password"; import { UserModel } from "@/api/models/user"; -import { CONFIG_PLUGIN } from "@/config"; import { SessionModel } from "../../../models/session"; import { @@ -36,7 +35,6 @@ export const zodSignUpSchema = z.object({ }); export const signUpRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: "Create a new user", diff --git a/packages/vitnode/src/api/modules/users/routes/time-zone.route.ts b/packages/vitnode/src/api/modules/users/routes/time-zone.route.ts index 0f46332e09..beb02d54d6 100644 --- a/packages/vitnode/src/api/modules/users/routes/time-zone.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/time-zone.route.ts @@ -2,14 +2,12 @@ import { z } from "@hono/zod-openapi"; import { eq } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; +import { buildRoute } from "@/api/builders"; import { isValidTimeZone } from "@/api/lib/notifications/digest-period"; -import { buildRoute } from "@/api/lib/route"; import { invalidateSessionCacheForUser } from "@/api/models/session-revoke"; -import { CONFIG_PLUGIN } from "@/config"; import { core_users } from "@/database/users"; export const updateMyTimeZoneRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "put", description: diff --git a/packages/vitnode/src/api/modules/users/routes/update-me.route.ts b/packages/vitnode/src/api/modules/users/routes/update-me.route.ts index 154594b0fd..5ba79c3154 100644 --- a/packages/vitnode/src/api/modules/users/routes/update-me.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/update-me.route.ts @@ -2,11 +2,10 @@ import { z } from "@hono/zod-openapi"; import { eq } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; +import { buildRoute } from "@/api/builders"; import { resolvePersonalInfoPolicy } from "@/api/lib/personal-info-policy"; -import { buildRoute } from "@/api/lib/route"; import { invalidateSessionCacheForUser } from "@/api/models/session-revoke"; import { SsoConnectionModel } from "@/api/models/sso-connection"; -import { CONFIG_PLUGIN } from "@/config"; import { core_users } from "@/database/users"; import { personalInformationChanges, @@ -54,7 +53,6 @@ export const zodPersonalInformation = z.object({ }); export const updateMeRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "patch", description: "Update the signed-in user's own personal information.", diff --git a/packages/vitnode/src/api/modules/users/routes/verify-email.route.ts b/packages/vitnode/src/api/modules/users/routes/verify-email.route.ts index 199845f0da..90ab6e2d52 100644 --- a/packages/vitnode/src/api/modules/users/routes/verify-email.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/verify-email.route.ts @@ -1,8 +1,7 @@ import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { EmailVerificationModel } from "@/api/models/email-verification"; -import { CONFIG_PLUGIN } from "@/config"; export const zodVerifyEmailSchema = z.object({ token: z @@ -14,7 +13,6 @@ export const zodVerifyEmailSchema = z.object({ }); export const verifyEmailRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: "Confirm an email address with the link sent at sign-up", diff --git a/packages/vitnode/src/api/modules/users/sso/connections/connections.module.ts b/packages/vitnode/src/api/modules/users/sso/connections/connections.module.ts index 1277085b27..ad2599156a 100644 --- a/packages/vitnode/src/api/modules/users/sso/connections/connections.module.ts +++ b/packages/vitnode/src/api/modules/users/sso/connections/connections.module.ts @@ -1,5 +1,4 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { authorizeSsoConnectionRoute } from "./routes/authorize.route"; import { ssoConnectionCallbackRoute } from "./routes/callback.route"; @@ -13,7 +12,6 @@ import { listSsoConnectionsRoute } from "./routes/list.route"; import { ssoPreferencesRoute } from "./routes/preferences.route"; export const ssoConnectionsModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "connections", routes: [ listSsoConnectionsRoute, diff --git a/packages/vitnode/src/api/modules/users/sso/connections/routes/authorize.route.ts b/packages/vitnode/src/api/modules/users/sso/connections/routes/authorize.route.ts index 2b7a90e220..0ca3177197 100644 --- a/packages/vitnode/src/api/modules/users/sso/connections/routes/authorize.route.ts +++ b/packages/vitnode/src/api/modules/users/sso/connections/routes/authorize.route.ts @@ -1,8 +1,7 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { SsoConnectionModel } from "@/api/models/sso-connection"; -import { CONFIG_PLUGIN } from "@/config"; import { SSO_CONNECTION_INTENTS, SSO_PROFILE_FIELDS } from "@/lib/sso-profile"; import { requireSignedInUser } from "../../../passkeys/failure"; @@ -19,7 +18,6 @@ export const zodSsoAuthorizeSchema = z.object({ }); export const authorizeSsoConnectionRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: diff --git a/packages/vitnode/src/api/modules/users/sso/connections/routes/callback.route.ts b/packages/vitnode/src/api/modules/users/sso/connections/routes/callback.route.ts index 366b8b65c8..192f5ea3d2 100644 --- a/packages/vitnode/src/api/modules/users/sso/connections/routes/callback.route.ts +++ b/packages/vitnode/src/api/modules/users/sso/connections/routes/callback.route.ts @@ -1,8 +1,7 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { SsoConnectionModel } from "@/api/models/sso-connection"; -import { CONFIG_PLUGIN } from "@/config"; import { SSO_CONNECTION_INTENTS } from "@/lib/sso-profile"; import { requireSignedInUser } from "../../../passkeys/failure"; @@ -19,7 +18,6 @@ export const zodSsoConnectionCallbackSchema = z.object({ }); export const ssoConnectionCallbackRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: diff --git a/packages/vitnode/src/api/modules/users/sso/connections/routes/disconnect.route.ts b/packages/vitnode/src/api/modules/users/sso/connections/routes/disconnect.route.ts index 0083045c12..e257d497bd 100644 --- a/packages/vitnode/src/api/modules/users/sso/connections/routes/disconnect.route.ts +++ b/packages/vitnode/src/api/modules/users/sso/connections/routes/disconnect.route.ts @@ -1,8 +1,7 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { SsoConnectionModel } from "@/api/models/sso-connection"; -import { CONFIG_PLUGIN } from "@/config"; import { requireSignedInUser } from "../../../passkeys/failure"; import { ssoConnectionFailure } from "../failure"; @@ -12,7 +11,6 @@ import { } from "../schema"; export const disconnectSsoConnectionRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "delete", description: diff --git a/packages/vitnode/src/api/modules/users/sso/connections/routes/import.route.ts b/packages/vitnode/src/api/modules/users/sso/connections/routes/import.route.ts index 700140c6a4..849f2b017f 100644 --- a/packages/vitnode/src/api/modules/users/sso/connections/routes/import.route.ts +++ b/packages/vitnode/src/api/modules/users/sso/connections/routes/import.route.ts @@ -1,8 +1,7 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { SsoConnectionModel } from "@/api/models/sso-connection"; -import { CONFIG_PLUGIN } from "@/config"; import { SSO_PROFILE_FIELDS } from "@/lib/sso-profile"; import { requireSignedInUser } from "../../../passkeys/failure"; @@ -18,7 +17,6 @@ import { const params = z.object({ providerId: zodSsoProviderIdParam }); export const ssoImportPreviewRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: @@ -56,7 +54,6 @@ export const zodSsoApplyImportSchema = z.object({ }); export const ssoApplyImportRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: @@ -98,7 +95,6 @@ export const ssoApplyImportRoute = buildRoute({ }); export const ssoDiscardImportRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "delete", description: "Throw away the signed-in user's pending import preview.", diff --git a/packages/vitnode/src/api/modules/users/sso/connections/routes/list.route.ts b/packages/vitnode/src/api/modules/users/sso/connections/routes/list.route.ts index 86f19f806c..d5c409f942 100644 --- a/packages/vitnode/src/api/modules/users/sso/connections/routes/list.route.ts +++ b/packages/vitnode/src/api/modules/users/sso/connections/routes/list.route.ts @@ -1,6 +1,5 @@ -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { SsoConnectionModel } from "@/api/models/sso-connection"; -import { CONFIG_PLUGIN } from "@/config"; import { requireSignedInUser } from "../../../passkeys/failure"; import { ssoConnectionFailure } from "../failure"; @@ -10,7 +9,6 @@ import { } from "../schema"; export const listSsoConnectionsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: diff --git a/packages/vitnode/src/api/modules/users/sso/connections/routes/preferences.route.ts b/packages/vitnode/src/api/modules/users/sso/connections/routes/preferences.route.ts index 1f1bbe38dd..00c75240c0 100644 --- a/packages/vitnode/src/api/modules/users/sso/connections/routes/preferences.route.ts +++ b/packages/vitnode/src/api/modules/users/sso/connections/routes/preferences.route.ts @@ -1,8 +1,7 @@ import { z } from "@hono/zod-openapi"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { SsoConnectionModel } from "@/api/models/sso-connection"; -import { CONFIG_PLUGIN } from "@/config"; import { requireSignedInUser } from "../../../passkeys/failure"; import { ssoConnectionFailure } from "../failure"; @@ -25,7 +24,6 @@ export const zodSsoPreferencesSchema = z.object({ }); export const ssoPreferencesRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "put", description: diff --git a/packages/vitnode/src/api/modules/users/sso/routes/callback.route.ts b/packages/vitnode/src/api/modules/users/sso/routes/callback.route.ts index f5205f11e7..f855eeffe9 100644 --- a/packages/vitnode/src/api/modules/users/sso/routes/callback.route.ts +++ b/packages/vitnode/src/api/modules/users/sso/routes/callback.route.ts @@ -1,12 +1,10 @@ import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { SessionModel } from "@/api/models/session"; import { SSOModel } from "@/api/models/sso"; -import { CONFIG_PLUGIN } from "@/config"; export const callbackRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", description: "SSO Callback", diff --git a/packages/vitnode/src/api/modules/users/sso/routes/create-url.route.ts b/packages/vitnode/src/api/modules/users/sso/routes/create-url.route.ts index 435e7b7337..d34e4cb323 100644 --- a/packages/vitnode/src/api/modules/users/sso/routes/create-url.route.ts +++ b/packages/vitnode/src/api/modules/users/sso/routes/create-url.route.ts @@ -1,11 +1,9 @@ import { z } from "zod"; -import { buildRoute } from "@/api/lib/route"; +import { buildRoute } from "@/api/builders"; import { SSOModel } from "@/api/models/sso"; -import { CONFIG_PLUGIN } from "@/config"; export const createUrlRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: "Generate SSO URL", diff --git a/packages/vitnode/src/api/modules/users/sso/routes/link.route.ts b/packages/vitnode/src/api/modules/users/sso/routes/link.route.ts index 24e3a2d5b6..0de6a8e3d9 100644 --- a/packages/vitnode/src/api/modules/users/sso/routes/link.route.ts +++ b/packages/vitnode/src/api/modules/users/sso/routes/link.route.ts @@ -1,10 +1,9 @@ import { z } from "zod"; +import { buildRoute } from "@/api/builders"; import { assertPasswordSignInEnabled } from "@/api/lib/password-sign-in"; -import { buildRoute } from "@/api/lib/route"; import { SessionModel } from "@/api/models/session"; import { SSOModel } from "@/api/models/sso"; -import { CONFIG_PLUGIN } from "@/config"; import { USER_PASSWORD_MAX_LENGTH } from "../../credential-limits"; @@ -18,7 +17,6 @@ export const zodSsoLinkSchema = z.object({ }); export const linkRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "post", description: diff --git a/packages/vitnode/src/api/modules/users/sso/sso.module.ts b/packages/vitnode/src/api/modules/users/sso/sso.module.ts index f252fffef9..a1fdbf0ba2 100644 --- a/packages/vitnode/src/api/modules/users/sso/sso.module.ts +++ b/packages/vitnode/src/api/modules/users/sso/sso.module.ts @@ -1,5 +1,4 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { ssoConnectionsModule } from "./connections/connections.module"; import { callbackRoute } from "./routes/callback.route"; @@ -7,7 +6,6 @@ import { createUrlRoute } from "./routes/create-url.route"; import { linkRoute } from "./routes/link.route"; export const ssoUserModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "sso", routes: [callbackRoute, createUrlRoute, linkRoute], modules: [ssoConnectionsModule], diff --git a/packages/vitnode/src/api/modules/users/users.module.ts b/packages/vitnode/src/api/modules/users/users.module.ts index b4d646263c..fbf6aa44b5 100644 --- a/packages/vitnode/src/api/modules/users/users.module.ts +++ b/packages/vitnode/src/api/modules/users/users.module.ts @@ -1,5 +1,4 @@ -import { buildModule } from "@/api/lib/module"; -import { CONFIG_PLUGIN } from "@/config"; +import { buildModule } from "@/api/builders"; import { userFilesModule } from "./files/files.module"; import { userImagesModule } from "./images/images.module"; @@ -22,7 +21,6 @@ import { verifyEmailRoute } from "./routes/verify-email.route"; import { ssoUserModule } from "./sso/sso.module"; export const usersModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "users", routes: [ sessionRoute, diff --git a/packages/vitnode/src/api/plugin.ts b/packages/vitnode/src/api/plugin.ts index edc0fe3c7f..1f51e43193 100644 --- a/packages/vitnode/src/api/plugin.ts +++ b/packages/vitnode/src/api/plugin.ts @@ -1,7 +1,7 @@ import type { ApiPluginContract } from "@/lib/fetcher/contract"; import { blocks } from "@/blocks/built-in"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import { settingsPage } from "@/views/auth/settings/widgets/settings-page"; import { @@ -26,7 +26,7 @@ import { searchModule } from "./modules/search/search.module"; import { usersModule } from "./modules/users/users.module"; export const newBuildPluginApiCore = buildApiPlugin({ - pluginId: CONFIG_PLUGIN.pluginId, + manifest, aiActions: [ altGenerateAiAction, altTranslateAiAction, @@ -100,6 +100,7 @@ export const newBuildPluginApiCore = buildApiPlugin({ { permission: "can_delete", dependsOn: ["can_view"] }, ], queue: ["can_view"], + plugins: ["can_view"], notifications: [ "can_view", { permission: "can_edit", dependsOn: ["can_view"] }, diff --git a/packages/vitnode/src/blocks/built-in/index.tsx b/packages/vitnode/src/blocks/built-in/index.tsx index 78d967df25..af43812831 100644 --- a/packages/vitnode/src/blocks/built-in/index.tsx +++ b/packages/vitnode/src/blocks/built-in/index.tsx @@ -1,6 +1,7 @@ +import { manifest } from "@/manifest"; + import type { BlockPluginSource } from "../types"; -import { CONFIG_PLUGIN } from "../../config"; import { ctaBlock } from "./cta"; import { heroBlock } from "./hero"; import { textBlock } from "./text"; @@ -10,7 +11,7 @@ export { heroBlock } from "./hero"; export { textBlock } from "./text"; export const blocks = { - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, blocks: [ctaBlock, heroBlock, textBlock], namespace: "core", } satisfies BlockPluginSource; diff --git a/packages/vitnode/src/components/ai/use-available-ai-actions.ts b/packages/vitnode/src/components/ai/use-available-ai-actions.ts index 4720824048..db2b3832a1 100644 --- a/packages/vitnode/src/components/ai/use-available-ai-actions.ts +++ b/packages/vitnode/src/components/ai/use-available-ai-actions.ts @@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import type { AiAssistScope } from "@/lib/ai/stream-client"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; export const useAvailableAiActions = (scope: AiAssistScope = "admin") => @@ -11,13 +11,13 @@ export const useAvailableAiActions = (scope: AiAssistScope = "admin") => const response = scope === "admin" ? await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "admin/ai", path: "/assist/available", }) : await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "ai", path: "/available", diff --git a/packages/vitnode/src/components/logo-vitnode.tsx b/packages/vitnode/src/components/logo-vitnode.tsx index 66c31706ad..c8c229cb36 100644 --- a/packages/vitnode/src/components/logo-vitnode.tsx +++ b/packages/vitnode/src/components/logo-vitnode.tsx @@ -107,41 +107,6 @@ export const LogoVitNode = ({ ); }; -/** - * VitNode's mark, at whichever of its two sizes the viewport has room for. - * - * One component rather than a `className` every header picks for itself, because - * the choice is not a size - it is *which mark*. The wordmark is 136px wide and - * the bar it sits in also holds the language switcher, the theme switcher and - * the visitor's avatar; below `sm` those leave it about 110px, so the wordmark - * either overflows the bar or has to be hidden, and hiding it is the one answer - * that is not allowed - a site with no mark on a phone reads as a broken header - * rather than a compact one. The hexagon says the same thing in 32px. - * - * ## Both are in the DOM, and only one is in the accessibility tree - * - * `hidden` is `display: none`, which removes an element from the accessibility - * tree as well as from the layout - so at any width exactly one `Logo VitNode` - * is exposed, and the link wrapping this has one accessible name rather than - * two. That is worth stating because the obvious alternative - `opacity-0`, or - * `sr-only` on the one that is not showing - would leave both, and a screen - * reader would announce the mark twice. - * - * Takes no props on purpose. It is a brand lockup, not a styled primitive: an - * application whose mark is not VitNode's passes its own element to the header's - * `logo` slot, and one that wants VitNode's at some other size renders - * {@link LogoVitNode} directly. - */ export const LogoVitNodeBrand = () => ( - <> - - - + ); diff --git a/packages/vitnode/src/components/ui/dynamic-icon.test.tsx b/packages/vitnode/src/components/ui/dynamic-icon.test.tsx index 769278c52a..91d7f1b13f 100644 --- a/packages/vitnode/src/components/ui/dynamic-icon.test.tsx +++ b/packages/vitnode/src/components/ui/dynamic-icon.test.tsx @@ -76,3 +76,17 @@ test("renders nothing for an unknown icon", async () => { expect(html).toBe(""); }); + +test("renders `missing` for an unknown icon", async () => { + const html = await renderToHtml( + + } + missing={} + name="not-a-real-icon" + /> + , + ); + + expect(html).toBe(''); +}); diff --git a/packages/vitnode/src/components/ui/dynamic-icon.tsx b/packages/vitnode/src/components/ui/dynamic-icon.tsx index a8511557af..1ae1d30250 100644 --- a/packages/vitnode/src/components/ui/dynamic-icon.tsx +++ b/packages/vitnode/src/components/ui/dynamic-icon.tsx @@ -20,9 +20,13 @@ export interface DynamicIconProps { export const DynamicIcon = ({ className, fallback, + missing = null, name, ...props -}: DynamicIconProps & { fallback?: React.ReactNode }) => { +}: DynamicIconProps & { + fallback?: React.ReactNode; + missing?: React.ReactNode; +}) => { const collector = React.use(LucideIconCollectorContext); const icon = React.useSyncExternalStore( subscribeLucideIcons, @@ -46,7 +50,7 @@ export const DynamicIcon = ({ collector?.collect(name, icon); - if (icon === null) return null; + if (icon === null) return <>{missing}; return ; }; diff --git a/packages/vitnode/src/config.ts b/packages/vitnode/src/config.ts deleted file mode 100644 index 14f5cb8fa4..0000000000 --- a/packages/vitnode/src/config.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const CONFIG_PLUGIN = { - pluginId: "@vitnode/core" as const, - version: "2.0.0-canary.13", -}; diff --git a/packages/vitnode/src/content/server/bind-content-plugin.ts b/packages/vitnode/src/content/server/bind-content-plugin.ts new file mode 100644 index 0000000000..c697445257 --- /dev/null +++ b/packages/vitnode/src/content/server/bind-content-plugin.ts @@ -0,0 +1,27 @@ +import type { PluginManifest } from "../../lib/plugin-manifest"; +import type { AnyContentContainerDefinition } from "../containers/types"; +import type { AnyContentModel } from "./model"; + +import { buildContentAdminModule } from "./module"; +import { buildContentPublicModule } from "./public-module"; + +export const bindContentPlugin = ( + manifest: Pick, "id">, +) => ({ + buildContentAdminModule: (options: { + containers?: AnyContentContainerDefinition[]; + contentTypes: AnyContentModel[]; + }) => buildContentAdminModule({ ...options, pluginId: manifest.id }), + buildContentPublicModule: < + const Models extends readonly AnyContentModel[], + const Containers extends readonly AnyContentContainerDefinition[] = + readonly [], + >(options: { + containers?: Containers; + contentTypes: Models; + }) => + buildContentPublicModule({ + ...options, + pluginId: manifest.id, + }), +}); diff --git a/packages/vitnode/src/content/server/index.ts b/packages/vitnode/src/content/server/index.ts index 865d88caf7..ba33b0a60f 100644 --- a/packages/vitnode/src/content/server/index.ts +++ b/packages/vitnode/src/content/server/index.ts @@ -35,6 +35,7 @@ export type { ContentContainerUsageCheck, } from "../containers/server/service"; export { CONTENT_SYSTEM_ACTOR, resolveContentActor } from "./actor"; +export { bindContentPlugin } from "./bind-content-plugin"; export { buildContentColumn, buildEditorialColumns, diff --git a/packages/vitnode/src/content/server/module.test.ts b/packages/vitnode/src/content/server/module.test.ts index f8cb98afb2..0009dd4b0c 100644 --- a/packages/vitnode/src/content/server/module.test.ts +++ b/packages/vitnode/src/content/server/module.test.ts @@ -5,6 +5,7 @@ import { testPostContentType, testSearchablePostContentType, } from "@/tests/content-fixtures"; +import { testManifest } from "@/tests/manifest"; import { buildApiPlugin } from "../../api/lib/plugin"; import { createContentModel } from "./model"; @@ -43,7 +44,7 @@ describe("buildContentAdminModule search indexers", () => { expect(generated).toBeDefined(); const plugin = buildApiPlugin({ - pluginId: PLUGIN_ID, + manifest: testManifest(PLUGIN_ID), modules: [adminModule([searchablePosts])], }); @@ -54,7 +55,7 @@ describe("buildContentAdminModule search indexers", () => { it("keeps a manual indexer alongside a generated one", () => { const plugin = buildApiPlugin({ - pluginId: PLUGIN_ID, + manifest: testManifest(PLUGIN_ID), modules: [adminModule([categories, posts, searchablePosts])], searchIndexers: [ { @@ -74,7 +75,7 @@ describe("buildContentAdminModule search indexers", () => { it("rejects a manual indexer that collides with a generated one", () => { expect(() => buildApiPlugin({ - pluginId: PLUGIN_ID, + manifest: testManifest(PLUGIN_ID), modules: [adminModule([searchablePosts])], searchIndexers: [ { @@ -89,7 +90,7 @@ describe("buildContentAdminModule search indexers", () => { it("reaches the plugin through the nested module tree", () => { const plugin = buildApiPlugin({ - pluginId: PLUGIN_ID, + manifest: testManifest(PLUGIN_ID), modules: [adminModule([categories, posts, searchablePosts])], }); diff --git a/packages/vitnode/src/framework/plugin-inventory/generate.test.ts b/packages/vitnode/src/framework/plugin-inventory/generate.test.ts new file mode 100644 index 0000000000..02cb64603b --- /dev/null +++ b/packages/vitnode/src/framework/plugin-inventory/generate.test.ts @@ -0,0 +1,101 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +import type { FrontendPluginInventoryEntry } from "../../lib/plugin-manifest.js"; + +import { + generatePluginInventorySource, + pluginInventoryFromLoadedConfig, +} from "./generate.js"; + +const SOURCE = "src/vitnode.config.ts"; + +const BLOG_MANIFEST = { + author: null, + description: "Posts", + icon: "newspaper", + id: "@acme/blog", + license: null, + title: "Blog", + version: "1.0.0", +}; + +describe("pluginInventoryFromLoadedConfig", () => { + it("lists each registration with its manifest, or null without one", () => { + expect( + pluginInventoryFromLoadedConfig( + [ + { manifest: BLOG_MANIFEST, pluginId: "@acme/blog", routes: {} }, + { pluginId: "@acme/shop" }, + ], + SOURCE, + ), + ).toEqual([ + { id: "@acme/blog", manifest: BLOG_MANIFEST }, + { id: "@acme/shop", manifest: null }, + ]); + }); + + it.each([ + ["a string", "Blog"], + ["a missing title", { ...BLOG_MANIFEST, title: undefined }], + ["a missing icon", { ...BLOG_MANIFEST, icon: undefined }], + ["a numeric version", { ...BLOG_MANIFEST, version: 1 }], + ["an undefined description", { ...BLOG_MANIFEST, description: undefined }], + ])("rejects manifest that is %s", (_, manifest) => { + expect(() => + pluginInventoryFromLoadedConfig( + [{ manifest, pluginId: "@acme/blog" }], + SOURCE, + ), + ).toThrow(/vitNodeConfig.plugins\[0\].manifest.*is not a plugin manifest/); + }); + + it("rejects manifest generated for another package", () => { + expect(() => + pluginInventoryFromLoadedConfig( + [{ manifest: BLOG_MANIFEST, pluginId: "@acme/shop" }], + SOURCE, + ), + ).toThrow( + /registers the plugin "@acme\/shop" with the manifest of "@acme\/blog"/, + ); + }); +}); + +describe("generatePluginInventorySource", () => { + const blog: FrontendPluginInventoryEntry = { + id: "@acme/blog", + manifest: BLOG_MANIFEST, + }; + const shop: FrontendPluginInventoryEntry = { + id: "@acme/shop", + manifest: null, + }; + const forum: FrontendPluginInventoryEntry = { + id: "@acme/forum", + manifest: { ...BLOG_MANIFEST, icon: null, id: "@acme/forum" }, + }; + + it("writes the same bytes whatever order the plugins are configured in", () => { + expect(generatePluginInventorySource([forum, shop, blog])).toBe( + generatePluginInventorySource([shop, blog, forum]), + ); + }); + + it("evaluates to the entries, sorted by id", async () => { + const dir = mkdtempSync(join(tmpdir(), "vitnode-plugin-inventory-")); + const path = join(dir, "plugin-inventory.gen.ts"); + writeFileSync(path, generatePluginInventorySource([shop, blog, forum])); + + try { + const module = (await import(path)) as { frontendPlugins: unknown }; + + expect(module.frontendPlugins).toEqual([blog, forum, shop]); + } finally { + rmSync(dir, { force: true, recursive: true }); + } + }); +}); diff --git a/packages/vitnode/src/framework/plugin-inventory/generate.ts b/packages/vitnode/src/framework/plugin-inventory/generate.ts new file mode 100644 index 0000000000..08ead04ff3 --- /dev/null +++ b/packages/vitnode/src/framework/plugin-inventory/generate.ts @@ -0,0 +1,148 @@ +import type { + FrontendPluginInventoryEntry, + PluginAuthor, + PluginManifest, +} from "../../lib/plugin-manifest.js"; + +import { assertPluginManifest } from "../../lib/plugin-manifest.js"; +import { PLUGIN_ROUTES_ERROR_PREFIX as ERROR_PREFIX } from "../plugin-routes/diagnostics.js"; + +export const PLUGIN_MANIFEST_SUBPATH = "manifest.gen"; + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null; + +const isNullableString = (value: unknown): value is null | string => + value === null || typeof value === "string"; + +const isAuthor = (value: unknown): value is PluginAuthor => + isRecord(value) && + typeof value.name === "string" && + isNullableString(value.email) && + isNullableString(value.url); + +const manifestFrom = ( + value: unknown, + pluginId: string, + where: string, +): null | PluginManifest => { + if (value === undefined) return null; + + if ( + !isRecord(value) || + typeof value.id !== "string" || + typeof value.title !== "string" || + !isNullableString(value.icon) || + !isNullableString(value.description) || + !isNullableString(value.version) || + !(value.license === undefined || isNullableString(value.license)) || + !( + value.author === undefined || + value.author === null || + isAuthor(value.author) + ) + ) { + throw new Error( + `${ERROR_PREFIX} ${where} is not a plugin manifest. Pass \`manifest\` from the plugin's generated \`src/manifest.gen.ts\`.`, + ); + } + + const manifest: PluginManifest = { + author: + value.author === undefined || value.author === null + ? null + : { + email: value.author.email, + name: value.author.name, + url: value.author.url, + }, + description: value.description, + icon: value.icon, + id: value.id, + license: value.license ?? null, + title: value.title, + version: value.version, + }; + assertPluginManifest(pluginId, manifest, where); + + return manifest; +}; + +export const pluginInventoryFromLoadedConfig = ( + plugins: readonly unknown[], + source: string, +): FrontendPluginInventoryEntry[] => + plugins.flatMap((plugin, index) => { + if (!isRecord(plugin) || typeof plugin.pluginId !== "string") return []; + + return [ + { + id: plugin.pluginId, + manifest: manifestFrom( + plugin.manifest, + plugin.pluginId, + `\`vitNodeConfig.plugins[${String(index)}].manifest\` in ${source}`, + ), + }, + ]; + }); + +const HEADER = `/* oxlint-disable */ + +// This file is generated by VitNode. Do not edit it, and do not format it. +// +// It is rewritten by the \`vitnode:plugin-routes\` Vite plugin on every +// \`vite dev\` and \`vite build\`, from the plugins configured in +// \`src/vitnode.config.ts\` and the manifest each one registers. +// +// Plain data and nothing else: no plugin code is imported, so the API can read +// it without loading the frontend. Pass it to +// \`buildApiConfig({ frontendPlugins })\` so AdminCP → System → Plugins lists +// what this web build runs, frontend-only plugins included. +// +// Same plugin configuration in, same bytes out: the entries are sorted by +// plugin id. +`; + +const literal = (value: null | string): string => + value === null ? "null" : JSON.stringify(value); + +const entrySource = ({ id, manifest }: FrontendPluginInventoryEntry): string => + manifest === null + ? ` { id: ${literal(id)}, manifest: null },\n` + : ` { + id: ${literal(id)}, + manifest: { + id: ${literal(manifest.id)}, + title: ${literal(manifest.title)}, + icon: ${literal(manifest.icon)}, + description: ${literal(manifest.description)}, + author: ${ + manifest.author === null + ? "null" + : `{ + name: ${literal(manifest.author.name)}, + email: ${literal(manifest.author.email)}, + url: ${literal(manifest.author.url)}, + }` + }, + license: ${literal(manifest.license)}, + version: ${literal(manifest.version)}, + }, + }, +`; + +export const generatePluginInventorySource = ( + entries: readonly FrontendPluginInventoryEntry[], +): string => { + const sorted = [...entries].sort((a, b) => + a.id === b.id ? 0 : a.id < b.id ? -1 : 1, + ); + + return `${HEADER} +import type { FrontendPluginInventory } from '@vitnode/core/lib/plugin-manifest' + +export const frontendPlugins: FrontendPluginInventory = [ +${sorted.map(entrySource).join("")}] +`; +}; diff --git a/packages/vitnode/src/framework/plugin-inventory/index.ts b/packages/vitnode/src/framework/plugin-inventory/index.ts new file mode 100644 index 0000000000..0967646228 --- /dev/null +++ b/packages/vitnode/src/framework/plugin-inventory/index.ts @@ -0,0 +1,5 @@ +export { + generatePluginInventorySource, + PLUGIN_MANIFEST_SUBPATH, + pluginInventoryFromLoadedConfig, +} from "./generate.js"; diff --git a/packages/vitnode/src/framework/plugin-manifest/generate.test.ts b/packages/vitnode/src/framework/plugin-manifest/generate.test.ts new file mode 100644 index 0000000000..392ef0b626 --- /dev/null +++ b/packages/vitnode/src/framework/plugin-manifest/generate.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from "vitest"; + +import { pluginManifestFromPackageJson } from "./generate.js"; + +const SOURCE = "package.json"; + +describe("pluginManifestFromPackageJson", () => { + it("reads the package fields, trimmed, and ignores everything else", () => { + expect( + pluginManifestFromPackageJson( + { + description: " Posts and categories ", + name: "@acme/blog", + version: "1.2.0", + vitnode: { icon: "./assets/icon.svg", title: "Blog" }, + }, + SOURCE, + ), + ).toEqual({ + author: null, + description: "Posts and categories", + id: "@acme/blog", + license: null, + version: "1.2.0", + }); + }); + + it.each([ + ["missing", { name: "@acme/blog" }], + ["blank", { description: " ", name: "@acme/blog", version: "" }], + ["null", { description: null, name: "@acme/blog", version: null }], + ])("reads %s optional fields as unknown", (_, manifest) => { + expect(pluginManifestFromPackageJson(manifest, SOURCE)).toEqual({ + author: null, + description: null, + id: "@acme/blog", + license: null, + version: null, + }); + }); + + it.each([ + ["a non-object manifest", ["@acme/blog"], /is not a JSON object/], + ["a missing name", { version: "1.0.0" }, /has no `name`/], + ["a blank name", { name: " " }, /has no `name`/], + [ + "a non-string version", + { name: "@acme/blog", version: 1 }, + /`version`.*has to be a string/, + ], + [ + "a non-string description", + { description: ["Posts"], name: "@acme/blog" }, + /`description`.*has to be a string/, + ], + ])("rejects %s", (_, manifest, error) => { + expect(() => pluginManifestFromPackageJson(manifest, SOURCE)).toThrow( + error, + ); + }); + + it("names the file and the manifest prefix in its errors", () => { + expect(() => + pluginManifestFromPackageJson({}, "plugins/blog/package.json"), + ).toThrow(/^\[VitNode plugin manifest\] plugins\/blog\/package.json/); + }); +}); diff --git a/packages/vitnode/src/framework/plugin-manifest/generate.ts b/packages/vitnode/src/framework/plugin-manifest/generate.ts new file mode 100644 index 0000000000..20726e4635 --- /dev/null +++ b/packages/vitnode/src/framework/plugin-manifest/generate.ts @@ -0,0 +1,136 @@ +import type { + PluginAuthor, + PluginManifest, +} from "../../lib/plugin-manifest.js"; + +import { PLUGIN_MANIFEST_ERROR_PREFIX as ERROR_PREFIX } from "../../lib/plugin-manifest.js"; + +export const PLUGIN_MANIFEST_PATH = "src/manifest.gen.ts"; + +export const PLUGIN_INFO_PATH = "src/plugin-info.ts"; + +export type PluginPackageManifest = Pick< + PluginManifest, + "author" | "description" | "id" | "license" | "version" +>; + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + +const optionalText = ( + value: unknown, + field: string, + source: string, +): null | string => { + if (value === undefined || value === null) return null; + if (typeof value !== "string") { + throw new Error( + `${ERROR_PREFIX} \`${field}\` in ${source} has to be a string.`, + ); + } + + const trimmed = value.trim(); + + return trimmed === "" ? null : trimmed; +}; + +const AUTHOR_STRING = /^([^<(]*?)\s*(?:<([^>]*)>)?\s*(?:\(([^)]*)\))?$/; + +const blankToNull = (value: string | undefined): null | string => { + const trimmed = value?.trim() ?? ""; + + return trimmed === "" ? null : trimmed; +}; + +const authorFrom = (value: unknown, source: string): null | PluginAuthor => { + if (value === undefined || value === null) return null; + + if (typeof value === "string") { + const match = AUTHOR_STRING.exec(value.trim()); + const name = blankToNull(match?.[1]); + + return name === null + ? null + : { + email: blankToNull(match?.[2]), + name, + url: blankToNull(match?.[3]), + }; + } + + if (!isRecord(value)) { + throw new Error( + `${ERROR_PREFIX} \`author\` in ${source} has to be a string or an object with \`name\`, \`email\` and \`url\`.`, + ); + } + + const name = optionalText(value.name, "author.name", source); + if (name === null) { + throw new Error(`${ERROR_PREFIX} \`author\` in ${source} has no \`name\`.`); + } + + return { + email: optionalText(value.email, "author.email", source), + name, + url: optionalText(value.url, "author.url", source), + }; +}; + +export const pluginManifestFromPackageJson = ( + manifest: unknown, + source: string, +): PluginPackageManifest => { + if (!isRecord(manifest)) { + throw new Error(`${ERROR_PREFIX} ${source} is not a JSON object.`); + } + + const id = optionalText(manifest.name, "name", source); + if (id === null) { + throw new Error( + `${ERROR_PREFIX} ${source} has no \`name\`. A plugin's id is its package name, so it needs one.`, + ); + } + + return { + author: authorFrom(manifest.author, source), + description: optionalText(manifest.description, "description", source), + id, + license: optionalText(manifest.license, "license", source), + version: optionalText(manifest.version, "version", source), + }; +}; + +const literal = (value: null | string): string => + value === null ? "null" : JSON.stringify(value); + +const authorLiteral = (author: null | PluginAuthor): string => + author === null + ? "null" + : `{ + name: ${literal(author.name)}, + email: ${literal(author.email)}, + url: ${literal(author.url)}, + }`; + +export const generatePluginManifestSource = ( + fields: PluginPackageManifest, + { hasInfo }: { hasInfo: boolean }, +): string => `/* oxlint-disable */ + +// This file is generated by VitNode. Do not edit it, and do not format it. +// +// It is rewritten by \`vitnode build\` and \`vitnode dev\` from this package's +// package.json - \`name\`, \`version\`, \`description\`, \`author\` and \`license\` - and the title and +// icon in \`src/plugin-info.ts\`. Pass \`manifest\` to \`buildPlugin\`, +// \`buildApiPlugin\` and \`bindApiPlugin\` - it is where the plugin id lives. +${hasInfo ? `\nimport { pluginInfo } from "./plugin-info"\n` : ""} +export const manifest = { + id: ${literal(fields.id)}, + title: ${hasInfo ? `pluginInfo.title ?? ${literal(fields.id)}` : literal(fields.id)}, + icon: ${hasInfo ? "pluginInfo.icon ?? null" : "null"}, + description: ${literal(fields.description)}, + author: ${authorLiteral(fields.author)}, + license: ${literal(fields.license)}, + version: ${literal(fields.version)}, +} as const +`; diff --git a/packages/vitnode/src/framework/plugin-manifest/index.ts b/packages/vitnode/src/framework/plugin-manifest/index.ts new file mode 100644 index 0000000000..4e4118f54c --- /dev/null +++ b/packages/vitnode/src/framework/plugin-manifest/index.ts @@ -0,0 +1,7 @@ +export { + generatePluginManifestSource, + PLUGIN_INFO_PATH, + PLUGIN_MANIFEST_PATH, + pluginManifestFromPackageJson, +} from "./generate.js"; +export type { PluginPackageManifest } from "./generate.js"; diff --git a/packages/vitnode/src/framework/plugin-routes/index.ts b/packages/vitnode/src/framework/plugin-routes/index.ts index 78a1d004e6..85fbc8e4c7 100644 --- a/packages/vitnode/src/framework/plugin-routes/index.ts +++ b/packages/vitnode/src/framework/plugin-routes/index.ts @@ -46,6 +46,7 @@ export { export { assertPluginId, + configuredPluginsFrom, pluginIdsFromLoadedConfig, pluginsFromLoadedConfig, routeDeclarationsFromRoutesModule, diff --git a/packages/vitnode/src/framework/plugin-routes/resolve.ts b/packages/vitnode/src/framework/plugin-routes/resolve.ts index 56340da4bc..fa5ec87633 100644 --- a/packages/vitnode/src/framework/plugin-routes/resolve.ts +++ b/packages/vitnode/src/framework/plugin-routes/resolve.ts @@ -48,19 +48,10 @@ export const toSingleQuotedLiteral = (value: string): string => .replace(/\n/g, "\\n") .replace(/\r/g, "\\r")}'`; -/** - * Every plugin an app configured, as much of each one as a build tool reads: - * its id, and the locale files it declares. - * - * Nothing here is evaluated beyond the config module itself - a route tree, an - * AdminCP navigation declaration and a content registration are all reached - * through their own package subpaths instead, and a locale file through the - * literal specifier this returns. - */ -export const pluginsFromLoadedConfig = ( +export const configuredPluginsFrom = ( loaded: unknown, source: string, -): PackageMessagesSource[] => { +): unknown[] => { if (!isRecord(loaded) || !isRecord(loaded.vitNodeConfig)) { throw new Error( `${ERROR_PREFIX} ${source} does not export \`vitNodeConfig\`. It has to, because the configured plugins are what the route registry is generated from.`, @@ -75,7 +66,23 @@ export const pluginsFromLoadedConfig = ( ); } - return plugins.map((plugin: unknown, index) => { + return plugins; +}; + +/** + * Every plugin an app configured, as much of each one as a build tool reads: + * its id, and the locale files it declares. + * + * Nothing here is evaluated beyond the config module itself - a route tree, an + * AdminCP navigation declaration and a content registration are all reached + * through their own package subpaths instead, and a locale file through the + * literal specifier this returns. + */ +export const pluginsFromLoadedConfig = ( + loaded: unknown, + source: string, +): PackageMessagesSource[] => + configuredPluginsFrom(loaded, source).map((plugin, index) => { if (!isRecord(plugin) || typeof plugin.pluginId !== "string") { throw new Error( `${ERROR_PREFIX} \`vitNodeConfig.plugins[${String(index)}]\` in ${source} has no string \`pluginId\`.`, @@ -90,7 +97,6 @@ export const pluginsFromLoadedConfig = ( pluginId: assertPluginId(plugin.pluginId, source), }; }); -}; export const pluginIdsFromLoadedConfig = ( loaded: unknown, diff --git a/packages/vitnode/src/framework/vite/plugin-routes.ts b/packages/vitnode/src/framework/vite/plugin-routes.ts index 07fb241a7b..591fd635df 100644 --- a/packages/vitnode/src/framework/vite/plugin-routes.ts +++ b/packages/vitnode/src/framework/vite/plugin-routes.ts @@ -14,6 +14,7 @@ import { } from "node:path"; import { pathToFileURL } from "node:url"; +import type { FrontendPluginInventoryEntry } from "../../lib/plugin-manifest"; import type { ResolvedPageCacheConfig } from "../../page-cache/config"; import type { ResolvedPageCacheRoute } from "../../page-cache/resolve"; import type { ResolvedAdminNavModule } from "../admin-nav"; @@ -45,8 +46,14 @@ import { generatePackageMessagesSource, resolvePackageMessagesModules, } from "../package-messages"; +import { + generatePluginInventorySource, + PLUGIN_MANIFEST_SUBPATH, + pluginInventoryFromLoadedConfig, +} from "../plugin-inventory"; import { compilePluginRoutes, + configuredPluginsFrom, contentTypesFromContentModule, CORE_PLUGIN_ID, hostRoutePathsFromFiles, @@ -107,6 +114,8 @@ const pathsFor = (appRoot: string) => ({ pageCache: join(appRoot, "src", "page-cache.gen.ts"), + pluginInventory: join(appRoot, "src", "plugin-inventory.gen.ts"), + registry: join(appRoot, "src", "plugin-routes.gen.ts"), staleManifest: join(appRoot, "src", "plugin-route-manifest.gen.ts"), @@ -130,6 +139,7 @@ export const resolverFor = (appRoot: string) => { interface AppConfig { i18n: LocaleRoutePathsConfig | undefined; + inventory: FrontendPluginInventoryEntry[]; pageCache: ResolvedPageCacheConfig; plugins: PackageMessagesSource[]; } @@ -143,11 +153,16 @@ const readAppConfig = async ( moduleCache: false, }); const loaded = await jiti.import(configPath); + const source = relative(appRoot, configPath); return { i18n: i18nFromLoadedConfig(loaded), + inventory: pluginInventoryFromLoadedConfig( + configuredPluginsFrom(loaded, source), + source, + ), pageCache: pageCacheConfigFromLoadedConfig(loaded), - plugins: pluginsFromLoadedConfig(loaded, relative(appRoot, configPath)), + plugins: pluginsFromLoadedConfig(loaded, source), }; }; @@ -472,7 +487,7 @@ const discover = async ( ) => { const paths = pathsFor(appRoot); const resolvePackageFile = resolverFor(appRoot); - const { i18n, pageCache, plugins } = await readAppConfig( + const { i18n, inventory, pageCache, plugins } = await readAppConfig( appRoot, paths.config, ); @@ -521,6 +536,10 @@ const discover = async ( WIDGETS_SUBPATH, resolvePackageFile, ); + const pluginManifests = readOptionalPluginModules<{ + pluginId: string; + specifier: string; + }>(pluginIds, PLUGIN_MANIFEST_SUBPATH, resolvePackageFile); const remainingPluginIds = pluginIds.filter( id => !pluginWidgets.modules.some(m => m.pluginId === id), ); @@ -542,6 +561,7 @@ const discover = async ( ...coreBlocks.watch, ...pluginBlocks.watch, ...contentModules.map(({ file }) => file), + ...pluginManifests.watch, ]; onLoaded?.(watch); @@ -590,6 +610,7 @@ const discover = async ( contentRegistry: contentRegistry.modules, hostRoutes, i18n, + inventory, packageMessages, pageCache, pageCacheRoutes, @@ -618,7 +639,7 @@ type DiscoveredRoutes = Omit< pageCacheRoutes: ResolvedPageCacheRoute[]; }; -/** All eight generated files, from one discovery pass. */ +/** All nine generated files, from one discovery pass. */ const writeGenerated = async ( appRoot: string, options: VitNodePluginRoutesOptions, @@ -634,6 +655,7 @@ const writeGenerated = async ( contentRegistry, hostRoutes, i18n, + inventory, packageMessages, pageCache, pageCacheRoutes, @@ -663,6 +685,10 @@ const writeGenerated = async ( paths.pageCache, generatePageCacheSource(pageCacheRouteTable(pageCacheRoutes)), ), + writeIfChanged( + paths.pluginInventory, + generatePluginInventorySource(inventory), + ), removeIfPresent(paths.staleManifest), ]); diff --git a/packages/vitnode/src/lib/ai/assist-client.ts b/packages/vitnode/src/lib/ai/assist-client.ts index 7cec892863..3b85bb3891 100644 --- a/packages/vitnode/src/lib/ai/assist-client.ts +++ b/packages/vitnode/src/lib/ai/assist-client.ts @@ -1,5 +1,5 @@ -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; export const AI_CLIENT_ERROR_CODES = [ "AI_ACTION_DISABLED", @@ -71,7 +71,7 @@ export const requestAiAssist = async ({ sourceFingerprint?: string; }): Promise<{ output: unknown; runId: number }> => { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { action, @@ -106,14 +106,14 @@ export const sendAiFeedback = async ({ const feedback = scope === "admin" ? fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args, method: "post", module: "admin/ai", path: "/assist/runs/{id}/feedback", }) : fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args, method: "post", module: "ai", diff --git a/packages/vitnode/src/lib/ai/stream-client.ts b/packages/vitnode/src/lib/ai/stream-client.ts index 6f4f1869ff..104c16c53f 100644 --- a/packages/vitnode/src/lib/ai/stream-client.ts +++ b/packages/vitnode/src/lib/ai/stream-client.ts @@ -1,5 +1,5 @@ -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; import { AiRequestError, @@ -38,7 +38,7 @@ export const streamAiAction = async ({ const response = scope === "admin" ? await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body }, method: "post", module: "admin/ai", @@ -46,7 +46,7 @@ export const streamAiAction = async ({ path: "/assist/stream", }) : await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body }, method: "post", module: "ai", @@ -96,14 +96,14 @@ export const estimateAiAction = async ({ const response = scope === "admin" ? await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body }, method: "post", module: "admin/ai", path: "/assist/estimate", }) : await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body }, method: "post", module: "ai", diff --git a/packages/vitnode/src/lib/i18n/sources.ts b/packages/vitnode/src/lib/i18n/sources.ts index ccee5c67db..0d37775bee 100644 --- a/packages/vitnode/src/lib/i18n/sources.ts +++ b/packages/vitnode/src/lib/i18n/sources.ts @@ -1,6 +1,6 @@ -import { CONFIG_PLUGIN } from "@/config"; import coreMessages from "@/locales"; import coreApiMessages from "@/locales/api"; +import { manifest } from "@/manifest"; import type { AppMessagesMap, @@ -71,7 +71,7 @@ const buildSources = ({ scope: string; }): MessagesSource[] => [ - { id: CONFIG_PLUGIN.pluginId, messages: coreBarrel }, + { id: manifest.id, messages: coreBarrel }, ...plugins.map(plugin => ({ id: plugin.pluginId, messages: plugin.messages, diff --git a/packages/vitnode/src/lib/navigation.ts b/packages/vitnode/src/lib/navigation.ts index 68bb726427..7a83dc2e7b 100644 --- a/packages/vitnode/src/lib/navigation.ts +++ b/packages/vitnode/src/lib/navigation.ts @@ -1,5 +1,4 @@ -import { CONFIG_PLUGIN } from "@/config"; - +import { manifest } from "@/manifest"; export const NAVIGATION_KINDS = ["preset", "custom"] as const; export type NavigationKind = (typeof NAVIGATION_KINDS)[number]; @@ -62,7 +61,7 @@ export interface PublicNavigationNode extends PublicNavigationItem { const CORE_NAVIGATION_NAMESPACE = "core.navigation"; export const navigationMessagesNamespace = (pluginId: string): string => - pluginId === CONFIG_PLUGIN.pluginId + pluginId === manifest.id ? CORE_NAVIGATION_NAMESPACE : `${pluginId}.navigation`; diff --git a/packages/vitnode/src/lib/plugin-manifest.ts b/packages/vitnode/src/lib/plugin-manifest.ts new file mode 100644 index 0000000000..ee7bfd1bd2 --- /dev/null +++ b/packages/vitnode/src/lib/plugin-manifest.ts @@ -0,0 +1,51 @@ +import { isLucideIconName } from "./emoji-icon"; + +export interface PluginInfo { + icon?: string; + title?: string; +} + +export interface PluginAuthor { + email: null | string; + name: string; + url: null | string; +} + +export interface PluginManifest { + author: null | PluginAuthor; + description: null | string; + icon: null | string; + id: Id; + license: null | string; + title: string; + version: null | string; +} + +export const PLUGIN_MANIFEST_ERROR_PREFIX = "[VitNode plugin manifest]"; + +export const assertPluginManifest = ( + pluginId: string, + manifest: Pick | undefined, + where: string, +): void => { + if (manifest === undefined) return; + + if (manifest.id !== pluginId) { + throw new Error( + `${PLUGIN_MANIFEST_ERROR_PREFIX} ${where} registers the plugin "${pluginId}" with the manifest of "${manifest.id}". The manifest comes from the package's own package.json, whose "name" is the plugin id - pass the \`manifest\` of this plugin's own \`src/manifest.gen.ts\`.`, + ); + } + + if (manifest.icon !== null && !isLucideIconName(manifest.icon)) { + throw new Error( + `${PLUGIN_MANIFEST_ERROR_PREFIX} ${where} gives the plugin "${pluginId}" the icon ${JSON.stringify(manifest.icon)}. An icon is a Lucide icon name such as "newspaper" - set it in the plugin's \`src/plugin-info.ts\`.`, + ); + } +}; + +export interface FrontendPluginInventoryEntry { + id: string; + manifest: null | PluginManifest; +} + +export type FrontendPluginInventory = readonly FrontendPluginInventoryEntry[]; diff --git a/packages/vitnode/src/lib/plugin.test.ts b/packages/vitnode/src/lib/plugin.test.ts new file mode 100644 index 0000000000..77b5b0bd31 --- /dev/null +++ b/packages/vitnode/src/lib/plugin.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; + +import type { PluginManifest } from "./plugin-manifest"; + +import { buildPlugin } from "./plugin"; + +const BLOG: PluginManifest<"@acme/blog"> = { + author: null, + description: null, + icon: "newspaper", + id: "@acme/blog", + license: null, + title: "Blog", + version: "1.0.0", +}; + +describe("buildPlugin manifest", () => { + it("takes its plugin id from the manifest", () => { + const plugin = buildPlugin({ manifest: BLOG }); + + expect(plugin.pluginId).toBe("@acme/blog"); + expect(plugin.manifest).toBe(BLOG); + }); + + it("refuses an icon that is not a Lucide icon name", () => { + expect(() => + buildPlugin({ manifest: { ...BLOG, icon: "./assets/icon.svg" } }), + ).toThrow( + /^\[VitNode plugin manifest\] buildPlugin\(\) gives the plugin "@acme\/blog" the icon "\.\/assets\/icon\.svg"/, + ); + }); +}); diff --git a/packages/vitnode/src/lib/plugin.ts b/packages/vitnode/src/lib/plugin.ts index 438d69c982..99f26d6a64 100644 --- a/packages/vitnode/src/lib/plugin.ts +++ b/packages/vitnode/src/lib/plugin.ts @@ -11,6 +11,9 @@ import type { PluginRoutes } from "../routing/tree"; import type { AdminNavItem as ResolvedAdminNavItem } from "../views/admin/layouts/sidebar/nav/nav-model"; import type { ContentFormSkeletonControl } from "../views/admin/views/content/form/skeleton"; import type { LocaleFilesMap, LocaleMessagesMap } from "./i18n/types"; +import type { PluginManifest } from "./plugin-manifest"; + +import { assertPluginManifest } from "./plugin-manifest"; export type AdminNavPermission = Omit; @@ -187,14 +190,25 @@ export interface BuildPluginReturn

{ * follow. */ localeFiles?: LocaleFilesMap; + manifest?: PluginManifest

; messages?: LocaleMessagesMap; pluginId: P; routes?: PluginRoutes; } -export function buildPlugin

( - props: BuildPluginReturn

, -): BuildPluginReturn

{ - return props; +export type BuildPluginOptions

= Omit< + BuildPluginReturn

, + "manifest" | "pluginId" +> & { + manifest: PluginManifest

; +}; + +export function buildPlugin({ + manifest, + ...props +}: BuildPluginOptions

): BuildPluginReturn

{ + assertPluginManifest(manifest.id, manifest, "buildPlugin()"); + + return { ...props, manifest, pluginId: manifest.id }; } diff --git a/packages/vitnode/src/locales/en.json b/packages/vitnode/src/locales/en.json index 03f5b93c74..566ee76d0f 100644 --- a/packages/vitnode/src/locales/en.json +++ b/packages/vitnode/src/locales/en.json @@ -40,6 +40,8 @@ "@vitnode/core:ai:can_manage": "Manage AI settings, models, pricing and access", "@vitnode/core:queue": "Queue Tasks", "@vitnode/core:queue:can_view": "View queue tasks", + "@vitnode/core:plugins": "Plugins", + "@vitnode/core:plugins:can_view": "View installed plugins and their versions", "@vitnode/core:notifications": "Notifications", "@vitnode/core:notifications:can_view": "View notification settings and delivery health", "@vitnode/core:notifications:can_edit": "Edit notification settings", @@ -2690,7 +2692,8 @@ "integrations": "Integrations", "navigation": "Navigation", "files": "Files", - "notifications": "Notifications" + "notifications": "Notifications", + "plugins": "Plugins" }, "advanced": { "title": "Advanced", @@ -3711,6 +3714,27 @@ } } } + }, + "plugins": { + "title": "Plugins", + "desc": "Every plugin this application is configured with, and the version it runs.", + "search": "Search plugins...", + "list": { + "plugin": "Plugin", + "version": "Version" + }, + "version_unknown": "Unknown version", + "no_description": "No description provided.", + "mismatch": "Version mismatch: the web app runs {frontend}, the API runs {api}. Rebuild and redeploy both so they match.", + "empty": { + "title": "No plugins installed", + "desc": "Add a plugin to vitnode.config.ts or vitnode.api.config.ts and it shows up here after the next build." + }, + "no_results": { + "title": "No plugins match your search", + "desc": "Try the plugin's name, its package ID or a word from its description.", + "clear": "Clear search" + } } }, "debug": { diff --git a/packages/vitnode/src/manifest.ts b/packages/vitnode/src/manifest.ts new file mode 100644 index 0000000000..98fad58946 --- /dev/null +++ b/packages/vitnode/src/manifest.ts @@ -0,0 +1,14 @@ +export const manifest = { + id: "@vitnode/core", + title: "VitNode", + icon: null, + description: + "Core package for VitNode, providing essential functionalities and configurations.", + author: { + name: "VitNode", + email: "contact@vitnode.com", + url: "https://vitnode.com", + }, + license: "MIT", + version: "2.0.0-canary.13", +} as const; diff --git a/packages/vitnode/src/pages/admin/system/plugins.tsx b/packages/vitnode/src/pages/admin/system/plugins.tsx new file mode 100644 index 0000000000..795348cf65 --- /dev/null +++ b/packages/vitnode/src/pages/admin/system/plugins.tsx @@ -0,0 +1,23 @@ +import type { PluginRoutePageProps } from "@/routing"; +import type { AdminPluginsRouteData } from "@/tanstack/admin/plugins/route"; + +import { adminBreadcrumb } from "@/tanstack/admin/breadcrumb"; +import { loadAdminPluginsRoute } from "@/tanstack/admin/plugins/route"; +import { AdminPluginsRouteContent } from "@/tanstack/admin/plugins/screen"; +import { defineAdminRoute } from "@/tanstack/plugin-routes"; + +const AdminPluginsPage = ({ + loaderData, +}: PluginRoutePageProps) => ( + +); + +export const route = defineAdminRoute({ + load: async ({ context, t }) => + await loadAdminPluginsRoute({ ...context, t }), + head: ({ loaderData }) => ({ ...loaderData }), + + breadcrumb: adminBreadcrumb({ segments: ["core", "system", "plugins"] }), +}); + +export default AdminPluginsPage; diff --git a/packages/vitnode/src/routes.tsx b/packages/vitnode/src/routes.tsx index d48fc9b9ca..5dfb64ba76 100644 --- a/packages/vitnode/src/routes.tsx +++ b/packages/vitnode/src/routes.tsx @@ -14,6 +14,7 @@ import { normalizeAdminFilesRouteSearch } from "./tanstack/admin/files/route-sea import { ADMIN_INTEGRATIONS_NAMESPACES } from "./tanstack/admin/integrations/route"; import { ADMIN_NAVIGATION_NAMESPACES } from "./tanstack/admin/navigation/route"; import { ADMIN_NOTIFICATIONS_NAMESPACES } from "./tanstack/admin/notifications/route"; +import { ADMIN_PLUGINS_NAMESPACES } from "./tanstack/admin/plugins/route"; import { ADMIN_QUEUE_NAMESPACES } from "./tanstack/admin/queue/route"; import { normalizeQueueRouteSearch } from "./tanstack/admin/queue/route-search"; import { ADMIN_ROLES_NAMESPACES } from "./tanstack/admin/roles/route"; @@ -326,6 +327,13 @@ export const routes = defineRoutes([ pendingComponent: CardsPendingSkeleton, }), + page("/admin/core/system/plugins", { + area: "admin", + component: lazy(() => import("./pages/admin/system/plugins")), + messages: ADMIN_PLUGINS_NAMESPACES, + pendingComponent: TablePendingSkeleton, + }), + page("/admin/core/system/notifications", { area: "admin", component: lazy(() => import("./pages/admin/system/notifications")), diff --git a/packages/vitnode/src/tanstack/admin/debug/screen.tsx b/packages/vitnode/src/tanstack/admin/debug/screen.tsx index 0bdc4a7736..8fcc5ac712 100644 --- a/packages/vitnode/src/tanstack/admin/debug/screen.tsx +++ b/packages/vitnode/src/tanstack/admin/debug/screen.tsx @@ -5,7 +5,7 @@ import type { DataTableNavigation } from "@/components/table/navigation"; import { DataTableNavigationProvider } from "@/components/table/navigation"; import { PageTitle } from "@/components/ui/page-title"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import { ClearCacheAction } from "@/views/admin/views/core/debug/actions/clear-cache/clear-cache"; import { QueueViewContent } from "@/views/admin/views/core/debug/queue/queue-view-content"; import { SystemLogsContent } from "@/views/admin/views/core/debug/system-logs/system-logs-content"; @@ -59,7 +59,7 @@ export const AdminDebugRouteContent = ({ diff --git a/packages/vitnode/src/tanstack/admin/default-transport.ts b/packages/vitnode/src/tanstack/admin/default-transport.ts index 3505f50db4..3155350402 100644 --- a/packages/vitnode/src/tanstack/admin/default-transport.ts +++ b/packages/vitnode/src/tanstack/admin/default-transport.ts @@ -1,4 +1,4 @@ -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import type { AdminSessionReadOptions } from "./session-read"; @@ -10,7 +10,7 @@ export const readAdminSessionFromApi = async ({ }: AdminSessionReadOptions = {}) => await readAdminSessionThrough(async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "admin", path: "/session", diff --git a/packages/vitnode/src/tanstack/admin/files/screen.tsx b/packages/vitnode/src/tanstack/admin/files/screen.tsx index d9c98a1177..908b1e22f3 100644 --- a/packages/vitnode/src/tanstack/admin/files/screen.tsx +++ b/packages/vitnode/src/tanstack/admin/files/screen.tsx @@ -5,7 +5,7 @@ import type { DataTableNavigation } from "@/components/table/navigation"; import { DataTableNavigationProvider } from "@/components/table/navigation"; import { PageTitle } from "@/components/ui/page-title"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import { FilesTableContent } from "@/views/admin/views/core/system/files/files-table-content"; import type { AdminTableNavigate } from "../table-search"; @@ -39,12 +39,12 @@ export const AdminFilesRouteContent = ({ const canDelete = useAdminPermission({ module: FILES_MODULE, permission: "can_delete", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, }); const canDownload = useAdminPermission({ module: FILES_MODULE, permission: "can_download", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, }); const navigation = React.useMemo( diff --git a/packages/vitnode/src/tanstack/admin/integrations/screen.tsx b/packages/vitnode/src/tanstack/admin/integrations/screen.tsx index ea72447941..56c77b6396 100644 --- a/packages/vitnode/src/tanstack/admin/integrations/screen.tsx +++ b/packages/vitnode/src/tanstack/admin/integrations/screen.tsx @@ -1,7 +1,7 @@ import { useSuspenseQuery } from "@tanstack/react-query"; import { PageTitle } from "@/components/ui/page-title"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import { IntegrationsContent } from "@/views/admin/views/core/system/integrations/integrations-content"; import { integrationsQueryOptions } from "@/views/admin/views/core/system/integrations/integrations-query"; import { sendTestEmailInBrowser } from "@/views/admin/views/core/system/integrations/send-test-email/send-test-email-mutation"; @@ -21,17 +21,17 @@ export const AdminIntegrationsRouteContent = ({ const canSendTestEmail = useAdminPermission({ module: SYSTEM_MODULE, permission: "can_send_test_email", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, }); const canTestStorage = useAdminPermission({ module: SYSTEM_MODULE, permission: "can_test_storage", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, }); const canTestAi = useAdminPermission({ module: SYSTEM_MODULE, permission: "can_test_ai", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, }); return ( diff --git a/packages/vitnode/src/tanstack/admin/notifications/screen.tsx b/packages/vitnode/src/tanstack/admin/notifications/screen.tsx index 9bd5c924b6..c95aad00f8 100644 --- a/packages/vitnode/src/tanstack/admin/notifications/screen.tsx +++ b/packages/vitnode/src/tanstack/admin/notifications/screen.tsx @@ -1,7 +1,7 @@ import { useSuspenseQuery } from "@tanstack/react-query"; import { PageTitle } from "@/components/ui/page-title"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import { NotificationsContent } from "@/views/admin/views/core/system/notifications/notifications-content"; import { notificationsOverviewQueryOptions } from "@/views/admin/views/core/system/notifications/notifications-query"; import { NotificationsSettingsSheet } from "@/views/admin/views/core/system/notifications/settings-sheet"; @@ -22,12 +22,12 @@ export const AdminNotificationsRouteContent = ({ const canEdit = useAdminPermission({ module: NOTIFICATIONS_MODULE, permission: "can_edit", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, }); const canManage = useAdminPermission({ module: NOTIFICATIONS_MODULE, permission: "can_manage", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, }); return ( diff --git a/packages/vitnode/src/tanstack/admin/plugins/index.ts b/packages/vitnode/src/tanstack/admin/plugins/index.ts new file mode 100644 index 0000000000..2021a23bfd --- /dev/null +++ b/packages/vitnode/src/tanstack/admin/plugins/index.ts @@ -0,0 +1,11 @@ +export type { AdminPluginsRouteData } from "./route"; +export { ADMIN_PLUGINS_NAMESPACES, loadAdminPluginsRoute } from "./route"; +export { AdminPluginsRouteContent } from "./screen"; +export type { + AdminInstalledPlugin, + AdminInstalledPlugins, +} from "@/views/admin/views/core/system/plugins/plugins-query"; +export { + installedPluginsQueryKey, + installedPluginsQueryOptions, +} from "@/views/admin/views/core/system/plugins/plugins-query"; diff --git a/packages/vitnode/src/tanstack/admin/plugins/route.tsx b/packages/vitnode/src/tanstack/admin/plugins/route.tsx new file mode 100644 index 0000000000..0d2e2d1cae --- /dev/null +++ b/packages/vitnode/src/tanstack/admin/plugins/route.tsx @@ -0,0 +1,42 @@ +import type { PluginRouteTranslator } from "@/routing"; + +import { installedPluginsQueryOptions } from "@/views/admin/views/core/system/plugins/plugins-query"; + +import type { AdminScreenContext } from "../screen"; + +import { requireAdminPermission } from "../screen"; + +export const ADMIN_PLUGINS_NAMESPACES = [ + "admin.system.plugins", + "core.global", +] as const; + +export interface AdminPluginsRouteData { + description: string; + title: string; +} + +const PLUGINS_VIEW_PERMISSION = { + module: "plugins", + permission: "can_view", +} as const; + +export const loadAdminPluginsRoute = async ({ + adminAccess, + queryClient, + t, +}: AdminScreenContext & { + t: PluginRouteTranslator; +}): Promise => { + requireAdminPermission(adminAccess, PLUGINS_VIEW_PERMISSION); + + await queryClient.query({ + ...installedPluginsQueryOptions(), + staleTime: "static", + }); + + return { + description: t("admin.system.plugins.desc"), + title: t("admin.system.plugins.title"), + }; +}; diff --git a/packages/vitnode/src/tanstack/admin/plugins/screen.tsx b/packages/vitnode/src/tanstack/admin/plugins/screen.tsx new file mode 100644 index 0000000000..185d109b4d --- /dev/null +++ b/packages/vitnode/src/tanstack/admin/plugins/screen.tsx @@ -0,0 +1,27 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; + +import { PageTitle } from "@/components/ui/page-title"; +import { PluginsContent } from "@/views/admin/views/core/system/plugins/plugins-content"; +import { installedPluginsQueryOptions } from "@/views/admin/views/core/system/plugins/plugins-query"; + +import type { AdminPluginsRouteData } from "./route"; + +import { RouteMessages } from "../../i18n/route-messages"; +import { ADMIN_PLUGINS_NAMESPACES } from "./route"; + +export const AdminPluginsRouteContent = ({ + description, + title, +}: AdminPluginsRouteData) => { + const { data } = useSuspenseQuery(installedPluginsQueryOptions()); + + return ( + +

+ + + +
+ + ); +}; diff --git a/packages/vitnode/src/tanstack/admin/server.ts b/packages/vitnode/src/tanstack/admin/server.ts index b42851c6b8..0e25d54d8b 100644 --- a/packages/vitnode/src/tanstack/admin/server.ts +++ b/packages/vitnode/src/tanstack/admin/server.ts @@ -1,6 +1,6 @@ +import { manifest } from "@/manifest"; import "@tanstack/react-start/server-only"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcher } from "@/tanstack/fetcher/server"; import { readAdminSessionThrough } from "./session-read"; @@ -8,7 +8,7 @@ import { readAdminSessionThrough } from "./session-read"; export const readAdminSessionOnApi = async () => await readAdminSessionThrough(async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "admin", path: "/session", diff --git a/packages/vitnode/src/tanstack/admin/state.ts b/packages/vitnode/src/tanstack/admin/state.ts index f6924c43a2..de190b9733 100644 --- a/packages/vitnode/src/tanstack/admin/state.ts +++ b/packages/vitnode/src/tanstack/admin/state.ts @@ -9,7 +9,7 @@ import { EMPTY_STAFF_PERMISSION_SET, hasStaffPermission, } from "@/api/lib/staff-permission"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; export const ADMIN_SESSION_QUERY_KEY = ["vitnode", "admin-session"] as const; @@ -111,7 +111,7 @@ export const hasAdminPermission = ( { module, permission, - plugin = CONFIG_PLUGIN.pluginId, + plugin = manifest.id, }: Omit & { plugin?: string }, ): boolean => hasStaffPermission(adminPermissionsOf(access), { diff --git a/packages/vitnode/src/tanstack/admin/user-search.ts b/packages/vitnode/src/tanstack/admin/user-search.ts index 477be9e965..5172e4dd18 100644 --- a/packages/vitnode/src/tanstack/admin/user-search.ts +++ b/packages/vitnode/src/tanstack/admin/user-search.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import type { AdminSearchUser } from "@/views/admin/layouts/search/search-users"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { MAX_SEARCH_RESULTS } from "@/views/admin/layouts/search/constants"; @@ -17,7 +17,7 @@ export const readAdminUserSearch = async ( try { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: { first: String(MAX_SEARCH_RESULTS), search: parsed.data }, }, diff --git a/packages/vitnode/src/tanstack/auth/default-transport.ts b/packages/vitnode/src/tanstack/auth/default-transport.ts index b4ea55ac95..aa4a5b6f21 100644 --- a/packages/vitnode/src/tanstack/auth/default-transport.ts +++ b/packages/vitnode/src/tanstack/auth/default-transport.ts @@ -1,4 +1,4 @@ -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { createAuthOperations } from "./transport-operations"; @@ -13,7 +13,7 @@ import { createAuthOperations } from "./transport-operations"; const operations = createAuthOperations({ changePasswordFromReset: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: data }, method: "post", module: "users", @@ -22,7 +22,7 @@ const operations = createAuthOperations({ completeSso: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { providerId: data.providerId }, query: { code: data.code, state: data.state }, @@ -34,7 +34,7 @@ const operations = createAuthOperations({ finishAdminPasskeySignIn: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: data }, method: "post", module: "users/passkeys", @@ -43,7 +43,7 @@ const operations = createAuthOperations({ finishPasskeySignIn: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: data }, method: "post", module: "users/passkeys", @@ -52,7 +52,7 @@ const operations = createAuthOperations({ linkSso: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { password: data.password, token: data.token }, params: { providerId: data.providerId }, @@ -64,7 +64,7 @@ const operations = createAuthOperations({ readSession: async () => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "users", path: "/session", @@ -72,7 +72,7 @@ const operations = createAuthOperations({ requestPasswordReset: async ({ captchaToken, email }) => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, captchaToken, args: { body: { email } }, method: "post", @@ -82,7 +82,7 @@ const operations = createAuthOperations({ resendEmailVerification: async ({ captchaToken, email }) => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, captchaToken, args: { body: { email } }, method: "post", @@ -92,7 +92,7 @@ const operations = createAuthOperations({ signIn: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: data }, method: "post", module: "users", @@ -101,7 +101,7 @@ const operations = createAuthOperations({ signOut: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { isAdmin: data.isAdmin ?? false } }, method: "delete", module: "users", @@ -110,7 +110,7 @@ const operations = createAuthOperations({ signUp: async ({ captchaToken, ...body }) => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, captchaToken, args: { body }, method: "post", @@ -120,7 +120,7 @@ const operations = createAuthOperations({ startAdminPasskeySignIn: async () => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "users/passkeys", path: "/admin-sign-in/options", @@ -128,7 +128,7 @@ const operations = createAuthOperations({ startPasskeySignIn: async () => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "users/passkeys", path: "/sign-in/options", @@ -136,7 +136,7 @@ const operations = createAuthOperations({ startSso: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { providerId: data.providerId } }, method: "post", module: "users/sso", @@ -145,7 +145,7 @@ const operations = createAuthOperations({ verifyEmail: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: data }, method: "post", module: "users", diff --git a/packages/vitnode/src/tanstack/auth/middleware-config.ts b/packages/vitnode/src/tanstack/auth/middleware-config.ts index c204584488..3f94d3696d 100644 --- a/packages/vitnode/src/tanstack/auth/middleware-config.ts +++ b/packages/vitnode/src/tanstack/auth/middleware-config.ts @@ -7,7 +7,7 @@ import type { routeMiddlewareSchema } from "@/api/modules/middleware/route"; import type { SSOProvider } from "@/views/auth/sso/providers"; import { seedLucideIcons } from "@/components/ui/icon-registry"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { normalizeSSOProviders } from "@/views/auth/sso/providers"; @@ -44,7 +44,7 @@ export const knownMiddlewareConfig = ( const fetchMiddlewareConfig = async (): Promise => { try { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "middleware", path: "/", diff --git a/packages/vitnode/src/tanstack/auth/server.ts b/packages/vitnode/src/tanstack/auth/server.ts index 92346f61aa..3d5af771fa 100644 --- a/packages/vitnode/src/tanstack/auth/server.ts +++ b/packages/vitnode/src/tanstack/auth/server.ts @@ -1,6 +1,6 @@ +import { manifest } from "@/manifest"; import "@tanstack/react-start/server-only"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcher } from "@/tanstack/fetcher/server"; import { createAuthOperations } from "./transport-operations"; @@ -17,7 +17,7 @@ import { createAuthOperations } from "./transport-operations"; const operations = createAuthOperations({ changePasswordFromReset: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: data }, method: "post", module: "users", @@ -26,7 +26,7 @@ const operations = createAuthOperations({ completeSso: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, allowSaveCookies: true, args: { params: { providerId: data.providerId }, @@ -39,7 +39,7 @@ const operations = createAuthOperations({ finishAdminPasskeySignIn: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, allowSaveCookies: true, args: { body: data }, method: "post", @@ -49,7 +49,7 @@ const operations = createAuthOperations({ finishPasskeySignIn: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, allowSaveCookies: true, args: { body: data }, method: "post", @@ -59,7 +59,7 @@ const operations = createAuthOperations({ linkSso: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, allowSaveCookies: true, args: { body: { password: data.password, token: data.token }, @@ -72,7 +72,7 @@ const operations = createAuthOperations({ readSession: async () => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "users", path: "/session", @@ -80,7 +80,7 @@ const operations = createAuthOperations({ requestPasswordReset: async ({ captchaToken, email }) => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, captchaToken, args: { body: { email } }, method: "post", @@ -90,7 +90,7 @@ const operations = createAuthOperations({ resendEmailVerification: async ({ captchaToken, email }) => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, captchaToken, args: { body: { email } }, method: "post", @@ -100,7 +100,7 @@ const operations = createAuthOperations({ signIn: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, allowSaveCookies: true, args: { body: data }, method: "post", @@ -110,7 +110,7 @@ const operations = createAuthOperations({ signOut: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, allowSaveCookies: true, args: { body: { isAdmin: data.isAdmin ?? false } }, method: "delete", @@ -120,7 +120,7 @@ const operations = createAuthOperations({ signUp: async ({ captchaToken, ...body }) => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, allowSaveCookies: true, captchaToken, args: { body }, @@ -131,7 +131,7 @@ const operations = createAuthOperations({ startAdminPasskeySignIn: async () => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, allowSaveCookies: true, method: "post", module: "users/passkeys", @@ -140,7 +140,7 @@ const operations = createAuthOperations({ startPasskeySignIn: async () => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, allowSaveCookies: true, method: "post", module: "users/passkeys", @@ -149,7 +149,7 @@ const operations = createAuthOperations({ startSso: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, allowSaveCookies: true, args: { params: { providerId: data.providerId } }, method: "post", @@ -159,7 +159,7 @@ const operations = createAuthOperations({ verifyEmail: async data => await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: data }, method: "post", module: "users", diff --git a/packages/vitnode/src/tanstack/fetcher/fetcher.test-d.ts b/packages/vitnode/src/tanstack/fetcher/fetcher.test-d.ts index 5db1877597..363a8e850e 100644 --- a/packages/vitnode/src/tanstack/fetcher/fetcher.test-d.ts +++ b/packages/vitnode/src/tanstack/fetcher/fetcher.test-d.ts @@ -11,6 +11,7 @@ import { testPostContentType, testTopicContentType, } from "@/tests/content-fixtures"; +import { testManifest } from "@/tests/manifest"; import { fetcher } from "./index"; @@ -106,7 +107,7 @@ const topicContent = createContentModel(testTopicContentType); const notesApiPlugin = () => buildApiPlugin({ - pluginId: PLUGIN_ID, + manifest: testManifest(PLUGIN_ID), modules: [ notesModule, buildContentPublicModule({ diff --git a/packages/vitnode/src/tanstack/i18n/messages.ts b/packages/vitnode/src/tanstack/i18n/messages.ts index 28ab056f91..9d28a1be97 100644 --- a/packages/vitnode/src/tanstack/i18n/messages.ts +++ b/packages/vitnode/src/tanstack/i18n/messages.ts @@ -1,6 +1,7 @@ -import "@tanstack/react-start/server-only"; import type { AbstractIntlMessages } from "use-intl"; +import "@tanstack/react-start/server-only"; + import type { AppMessagesMap, LocaleMessagesMap, @@ -8,10 +9,10 @@ import type { } from "@/lib/i18n/types"; import type { VitNodeServerConfig } from "@/vitnode.config"; -import { CONFIG_PLUGIN } from "@/config"; import { loadMessages } from "@/lib/i18n/load-messages"; import { pickMessages } from "@/lib/i18n/pick-messages"; import { buildAppMessagesSources } from "@/lib/i18n/sources"; +import { manifest } from "@/manifest"; import type { IntlMessages } from "./runtime"; @@ -33,8 +34,8 @@ export const buildBundledMessagesSources = ({ }: BundledMessagesOptions): MessagesSource[] => [ { - id: CONFIG_PLUGIN.pluginId, - messages: packageMessages[CONFIG_PLUGIN.pluginId], + id: manifest.id, + messages: packageMessages[manifest.id], }, ...plugins.map(({ pluginId }) => ({ id: pluginId, diff --git a/packages/vitnode/src/tanstack/widgets/query.ts b/packages/vitnode/src/tanstack/widgets/query.ts index 56aa279153..bb8b06c754 100644 --- a/packages/vitnode/src/tanstack/widgets/query.ts +++ b/packages/vitnode/src/tanstack/widgets/query.ts @@ -5,9 +5,9 @@ import type { EditablePageSavePayload, } from "@/content/editor"; -import { CONFIG_PLUGIN } from "@/config"; import { EditablePageSaveRefused } from "@/content/editor"; import { OPERATIONAL_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; export interface ModeratorPermissions { @@ -54,7 +54,7 @@ export const pageLayoutQueryOptions = (pageId: string) => queryOptions({ queryFn: async (): Promise => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "pages", path: "/layout", @@ -79,7 +79,7 @@ export const moderatorPermissionsQueryOptions = () => queryOptions({ queryFn: async (): Promise => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "users", path: "/permissions", @@ -130,7 +130,7 @@ export const savePageLayout = async ( payload: EditablePageSavePayload, ): Promise => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "put", module: "pages", path: "/layout", diff --git a/packages/vitnode/src/tests/manifest.ts b/packages/vitnode/src/tests/manifest.ts new file mode 100644 index 0000000000..7857077ca5 --- /dev/null +++ b/packages/vitnode/src/tests/manifest.ts @@ -0,0 +1,15 @@ +import type { PluginManifest } from "@/lib/plugin-manifest"; + +export const testManifest = ( + id: Id, + overrides: Partial, "id">> = {}, +): PluginManifest => ({ + author: null, + description: null, + icon: null, + id, + license: null, + title: id, + version: null, + ...overrides, +}); diff --git a/packages/vitnode/src/views/admin/layouts/search/multi-locale-titles.test.ts b/packages/vitnode/src/views/admin/layouts/search/multi-locale-titles.test.ts index 6c60662ee5..9f3dc23f7f 100644 --- a/packages/vitnode/src/views/admin/layouts/search/multi-locale-titles.test.ts +++ b/packages/vitnode/src/views/admin/layouts/search/multi-locale-titles.test.ts @@ -1,9 +1,9 @@ import { createTranslator } from "use-intl"; import { beforeEach, describe, expect, it } from "vitest"; -import { CONFIG_PLUGIN } from "@/config"; import { loadMessages, resetMessagesCache } from "@/lib/i18n/load-messages"; import { buildMessagesSources } from "@/lib/i18n/sources"; +import { manifest } from "@/manifest"; import { buildSearchText, matchesAdminNavItem } from "./flatten-nav"; @@ -14,7 +14,7 @@ const buildTranslator = async (locale: string) => { sources: buildMessagesSources({ appMessages: { pl: { - [CONFIG_PLUGIN.pluginId]: async () => + [manifest.id]: async () => Promise.resolve({ default: { admin: { diff --git a/packages/vitnode/src/views/admin/layouts/search/search-dialog-content.tsx b/packages/vitnode/src/views/admin/layouts/search/search-dialog-content.tsx index 8fedba3f1a..80c0de9bf9 100644 --- a/packages/vitnode/src/views/admin/layouts/search/search-dialog-content.tsx +++ b/packages/vitnode/src/views/admin/layouts/search/search-dialog-content.tsx @@ -16,7 +16,7 @@ import { CommandShortcut, } from "@/components/ui/command"; import { Skeleton } from "@/components/ui/skeleton"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import type { AdminSearchNavItem } from "./flatten-nav"; import type { AdminUserSearch } from "./search-users"; @@ -100,7 +100,7 @@ export const SearchAdminDialogContent = ({ ); const canViewUsers = useAdminStaffPermission({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, module: "users", permission: "can_view", }); diff --git a/packages/vitnode/src/views/admin/layouts/search/search-only-pages.tsx b/packages/vitnode/src/views/admin/layouts/search/search-only-pages.tsx index dbbf0f44f8..d89d6cbf98 100644 --- a/packages/vitnode/src/views/admin/layouts/search/search-only-pages.tsx +++ b/packages/vitnode/src/views/admin/layouts/search/search-only-pages.tsx @@ -5,7 +5,7 @@ import type { StaffPermissionSet } from "@/api/lib/permission-staff"; import type { AdminNavTranslator } from "@/views/admin/layouts/sidebar/nav/nav-model"; import { hasStaffPermission } from "@/api/lib/staff-permission"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import type { AdminSearchNavItem } from "./flatten-nav"; @@ -42,7 +42,7 @@ export const adminSearchOnlyItems = ({ return pages .filter(page => hasStaffPermission(permissions, { - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, ...page.permission, }), ) diff --git a/packages/vitnode/src/views/admin/layouts/sidebar/nav/nav-model.test.ts b/packages/vitnode/src/views/admin/layouts/sidebar/nav/nav-model.test.ts index 69cef7a1b8..27278b4378 100644 --- a/packages/vitnode/src/views/admin/layouts/sidebar/nav/nav-model.test.ts +++ b/packages/vitnode/src/views/admin/layouts/sidebar/nav/nav-model.test.ts @@ -60,9 +60,10 @@ describe("the core group", () => { expect(hrefs(nav)).toEqual([ "/admin/core/", "/admin/core/system", + "/admin/core/system/plugins", "/admin/core/system/integrations", - "/admin/core/system/navigation", "/admin/core/system/files", + "/admin/core/system/navigation", "/admin/core/system/notifications", "/admin/core/users", "/admin/core/users", @@ -137,7 +138,7 @@ describe("the core group", () => { expect(core.title).toBe("admin.global.nav.core"); expect(core.items[0].title).toBe("admin.global.nav.dashboard"); expect(core.items[1].items?.[0].title).toBe( - "admin.global.nav.system.integrations", + "admin.global.nav.system.plugins", ); }); }); diff --git a/packages/vitnode/src/views/admin/layouts/sidebar/nav/nav-model.tsx b/packages/vitnode/src/views/admin/layouts/sidebar/nav/nav-model.tsx index c5673f15a6..fa3346896f 100644 --- a/packages/vitnode/src/views/admin/layouts/sidebar/nav/nav-model.tsx +++ b/packages/vitnode/src/views/admin/layouts/sidebar/nav/nav-model.tsx @@ -16,7 +16,6 @@ import type { import type { VitNodeConfig } from "@/vitnode.config"; import { hasStaffPermission } from "@/api/lib/staff-permission"; -import { CONFIG_PLUGIN } from "@/config"; import { contentContainerAdminHref, contentContainerNamespace, @@ -29,6 +28,7 @@ import { } from "@/content/admin/labels"; import { CONTENT_PERMISSIONS } from "@/content/const"; import { contentAdminHref } from "@/content/registry"; +import { manifest } from "@/manifest"; import { fitNamespaceList, MAX_NAMESPACES } from "@/routing"; /** A resolved sub-item, as the sidebar and the search index read it. */ @@ -156,7 +156,7 @@ const core = ( ): PermissionsStaffArgs => ({ module, permission, - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, }); /** Core's own group, before the permission filter. */ @@ -174,21 +174,26 @@ const coreNavGroup = (): AdminNavGroupDeclaration => ({ title: key("admin.global.nav.system.title"), icon: , items: [ + { + title: key("admin.global.nav.system.plugins"), + href: "/admin/core/system/plugins", + permission: core("plugins"), + }, { title: key("admin.global.nav.system.integrations"), href: "/admin/core/system/integrations", permission: core("system"), }, - { - title: key("admin.global.nav.system.navigation"), - href: "/admin/core/system/navigation", - permission: core("navigation"), - }, { title: key("admin.global.nav.system.files"), href: "/admin/core/system/files", permission: core("files"), }, + { + title: key("admin.global.nav.system.navigation"), + href: "/admin/core/system/navigation", + permission: core("navigation"), + }, { title: key("admin.global.nav.system.notifications"), href: "/admin/core/system/notifications", diff --git a/packages/vitnode/src/views/admin/layouts/user-bar/user-bar-content.tsx b/packages/vitnode/src/views/admin/layouts/user-bar/user-bar-content.tsx index 8648ad935e..072f31e11b 100644 --- a/packages/vitnode/src/views/admin/layouts/user-bar/user-bar-content.tsx +++ b/packages/vitnode/src/views/admin/layouts/user-bar/user-bar-content.tsx @@ -28,8 +28,8 @@ import { useSidebar, } from "@/components/ui/sidebar"; import { UserMenuIdentity } from "@/components/user-menu-identity"; -import { CONFIG_PLUGIN } from "@/config"; import { VITNODE_ISSUES_URL, VITNODE_WEBSITE_URL } from "@/lib/docs-links"; +import { manifest } from "@/manifest"; export interface AdminUserBarUser { avatarColor: string; @@ -50,7 +50,7 @@ export const UserBarAdminContent = ({ const t = useTranslations("admin.global.nav.user_bar"); const { isMobile } = useSidebar(); const canViewDebug = useAdminStaffPermission({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, module: "debug", permission: "can_view", }); diff --git a/packages/vitnode/src/views/admin/views/core/advanced/cron/cron-query.ts b/packages/vitnode/src/views/admin/views/core/advanced/cron/cron-query.ts index e8f2f9db8f..2adec8dded 100644 --- a/packages/vitnode/src/views/admin/views/core/advanced/cron/cron-query.ts +++ b/packages/vitnode/src/views/admin/views/core/advanced/cron/cron-query.ts @@ -6,8 +6,8 @@ import type { AdminTableParams, } from "@/views/admin/table/params"; -import { CONFIG_PLUGIN } from "@/config"; import { OPERATIONAL_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { AdminRequestError, @@ -46,7 +46,7 @@ export type CronPageFetcher = (params: CronParams) => Promise; export const fetchCronPage: CronPageFetcher = async params => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: params }, method: "get", module: "admin/advanced/cron", @@ -97,7 +97,7 @@ export interface CronHealth { export const fetchCronHealth = async (): Promise => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "admin/advanced/cron", path: "/health", diff --git a/packages/vitnode/src/views/admin/views/core/advanced/cron/run-action/run-action.tsx b/packages/vitnode/src/views/admin/views/core/advanced/cron/run-action/run-action.tsx index e43bcc2528..6a1d02213c 100644 --- a/packages/vitnode/src/views/admin/views/core/advanced/cron/run-action/run-action.tsx +++ b/packages/vitnode/src/views/admin/views/core/advanced/cron/run-action/run-action.tsx @@ -6,7 +6,7 @@ import { useTranslations } from "use-intl"; import { useAdminStaffPermission } from "@/components/staff-permission/provider"; import { Button } from "@/components/ui/button"; import { TooltipWithContent } from "@/components/ui/tooltip"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import type { RunCron } from "./run-cron"; @@ -20,7 +20,7 @@ export const RunActionCronTable = ({ const t = useTranslations("admin.advanced.cron.list.actions.runNow"); const tError = useTranslations("core.global.errors"); const canRun = useAdminStaffPermission({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, module: "cron", permission: "can_run", }); diff --git a/packages/vitnode/src/views/admin/views/core/advanced/cron/run-action/run-cron.ts b/packages/vitnode/src/views/admin/views/core/advanced/cron/run-action/run-cron.ts index d671da2875..39a75b502b 100644 --- a/packages/vitnode/src/views/admin/views/core/advanced/cron/run-action/run-cron.ts +++ b/packages/vitnode/src/views/admin/views/core/advanced/cron/run-action/run-cron.ts @@ -1,5 +1,5 @@ -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; export type RunCronResult = undefined | { error?: string }; @@ -8,7 +8,7 @@ export type RunCron = (id: number) => Promise; export const runCronInBrowser: RunCron = async id => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id: String(id) } }, method: "post", module: "admin/advanced/cron", diff --git a/packages/vitnode/src/views/admin/views/core/advanced/queue/queue-query.ts b/packages/vitnode/src/views/admin/views/core/advanced/queue/queue-query.ts index 34585db346..91587ff8a8 100644 --- a/packages/vitnode/src/views/admin/views/core/advanced/queue/queue-query.ts +++ b/packages/vitnode/src/views/admin/views/core/advanced/queue/queue-query.ts @@ -6,8 +6,8 @@ import type { AdminTableParams, } from "@/views/admin/table/params"; -import { CONFIG_PLUGIN } from "@/config"; import { OPERATIONAL_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { AdminRequestError, @@ -60,7 +60,7 @@ export type QueuePageFetcher = (params: QueueParams) => Promise; /** One page, over whichever transport the host hands in. */ export const fetchQueuePage: QueuePageFetcher = async params => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: params }, method: "get", module: "admin/advanced/queue", diff --git a/packages/vitnode/src/views/admin/views/core/advanced/search/search-index-mutations.ts b/packages/vitnode/src/views/admin/views/core/advanced/search/search-index-mutations.ts index 32299a5632..f95b627a33 100644 --- a/packages/vitnode/src/views/admin/views/core/advanced/search/search-index-mutations.ts +++ b/packages/vitnode/src/views/admin/views/core/advanced/search/search-index-mutations.ts @@ -1,5 +1,5 @@ -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; /** What a mutation reports back. `error` is the API's own text. */ export interface SearchIndexMutationResult { @@ -26,7 +26,7 @@ export const rebuildSearchIndexInBrowser: RebuildSearchIndex = async itemType => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: itemType ? { itemType } : {} }, method: "post", module: "admin/debug", @@ -47,7 +47,7 @@ export const clearSearchCollectionInBrowser: ClearSearchCollection = async itemType => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { itemType } }, method: "post", module: "admin/debug", diff --git a/packages/vitnode/src/views/admin/views/core/advanced/search/search-index-query.ts b/packages/vitnode/src/views/admin/views/core/advanced/search/search-index-query.ts index 9fead86a3f..87de8a4717 100644 --- a/packages/vitnode/src/views/admin/views/core/advanced/search/search-index-query.ts +++ b/packages/vitnode/src/views/admin/views/core/advanced/search/search-index-query.ts @@ -1,7 +1,7 @@ import { queryOptions } from "@tanstack/react-query"; -import { CONFIG_PLUGIN } from "@/config"; import { OPERATIONAL_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { AdminRequestError } from "@/views/admin/admin-request"; import { adminQueryRoot } from "@/views/admin/table/query"; @@ -27,7 +27,7 @@ export type SearchIndexStatusFetcher = () => Promise; /** The status, over whichever transport the host hands in. */ export const fetchSearchIndexStatus: SearchIndexStatusFetcher = async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "admin/debug", path: "/search/status", diff --git a/packages/vitnode/src/views/admin/views/core/ai/ai-mutations.ts b/packages/vitnode/src/views/admin/views/core/ai/ai-mutations.ts index 2cfff201e7..5ea20af856 100644 --- a/packages/vitnode/src/views/admin/views/core/ai/ai-mutations.ts +++ b/packages/vitnode/src/views/admin/views/core/ai/ai-mutations.ts @@ -5,8 +5,8 @@ import type { zodAiSettings, } from "@/api/modules/admin/ai/schemas"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; import { type AdminMutationResult, runAdminApiMutation, @@ -43,7 +43,7 @@ export const updateAdminAiSettings = async ( parse: ok, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body }, method: "put", module: "admin/ai", @@ -60,7 +60,7 @@ export const updateAdminAiAction = async ( parse: ok, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body }, method: "put", module: "admin/ai", @@ -77,7 +77,7 @@ export const updateAdminAiRoleAccess = async ( parse: ok, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body }, method: "put", module: "admin/ai", @@ -94,7 +94,7 @@ export const updateAdminAiUserOverride = async ( parse: ok, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body }, method: "put", module: "admin/ai", @@ -111,7 +111,7 @@ export const deleteAdminAiUserOverride = async ( parse: ok, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: { userId } }, method: "delete", module: "admin/ai", diff --git a/packages/vitnode/src/views/admin/views/core/ai/ai-permissions.ts b/packages/vitnode/src/views/admin/views/core/ai/ai-permissions.ts index a245a82ec4..360d05eb70 100644 --- a/packages/vitnode/src/views/admin/views/core/ai/ai-permissions.ts +++ b/packages/vitnode/src/views/admin/views/core/ai/ai-permissions.ts @@ -1,11 +1,11 @@ import type { PermissionsStaffArgs } from "@/api/lib/permission-staff"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; const ai = (permission: string): PermissionsStaffArgs => ({ module: "ai", permission, - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, }); export const ADMIN_AI_PERMISSIONS = { diff --git a/packages/vitnode/src/views/admin/views/core/ai/ai-query.ts b/packages/vitnode/src/views/admin/views/core/ai/ai-query.ts index 21a0385740..b728d47981 100644 --- a/packages/vitnode/src/views/admin/views/core/ai/ai-query.ts +++ b/packages/vitnode/src/views/admin/views/core/ai/ai-query.ts @@ -17,11 +17,11 @@ import type { } from "@/views/admin/table/params"; import type { AdminIdentity } from "@/views/admin/views/core/shared/admin-scope"; -import { CONFIG_PLUGIN } from "@/config"; import { OPERATIONAL_STALE_TIME, RECORD_STALE_TIME, } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { AdminRequestError, @@ -180,7 +180,7 @@ export type AdminAiOverviewFetcher = ( export const fetchAdminAiOverview: AdminAiOverviewFetcher = async search => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: search }, method: "get", module: "admin/ai", @@ -202,7 +202,7 @@ export type AdminAiSettingsFetcher = () => Promise; export const fetchAdminAiSettings: AdminAiSettingsFetcher = async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "admin/ai", path: "/settings", @@ -219,7 +219,7 @@ export type AdminAiModelsFetcher = () => Promise<{ models: AdminAiModel[] }>; export const fetchAdminAiModels: AdminAiModelsFetcher = async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "admin/ai", path: "/models", @@ -238,7 +238,7 @@ export type AdminAiActionsFetcher = () => Promise<{ export const fetchAdminAiActions: AdminAiActionsFetcher = async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "admin/ai", path: "/actions", @@ -258,7 +258,7 @@ export type AdminAiRoleAccessFetcher = ( export const fetchAdminAiRoleAccess: AdminAiRoleAccessFetcher = async roleId => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: roleId === null ? {} : { roleId } }, method: "get", module: "admin/ai", @@ -283,7 +283,7 @@ export type AdminAiUserOverrideFetcher = ( export const fetchAdminAiUserOverride: AdminAiUserOverrideFetcher = async userId => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { userId } }, method: "get", module: "admin/ai", @@ -307,7 +307,7 @@ export type AdminAiHistoryFetcher = ( export const fetchAdminAiHistory: AdminAiHistoryFetcher = async params => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: params }, method: "get", module: "admin/ai", @@ -329,7 +329,7 @@ export type AdminAiRunFetcher = (id: number) => Promise; export const fetchAdminAiRun: AdminAiRunFetcher = async id => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id } }, method: "get", module: "admin/ai", diff --git a/packages/vitnode/src/views/admin/views/core/dashboard/widgets/layout-query.ts b/packages/vitnode/src/views/admin/views/core/dashboard/widgets/layout-query.ts index 58bc64cfe6..da6278050a 100644 --- a/packages/vitnode/src/views/admin/views/core/dashboard/widgets/layout-query.ts +++ b/packages/vitnode/src/views/admin/views/core/dashboard/widgets/layout-query.ts @@ -3,9 +3,9 @@ import { queryOptions } from "@tanstack/react-query"; import type { AdminDashboardWidgetLayoutItem } from "@/database/dashboard"; import type { AdminIdentity } from "@/views/admin/views/core/shared/admin-scope"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { ADMIN_DASHBOARD_SCREEN, @@ -24,7 +24,7 @@ export type DashboardLayoutFetcher = () => Promise; /** The stored layout, over whichever transport the host hands in. */ export const fetchDashboardLayout: DashboardLayoutFetcher = async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "admin/dashboard", path: "/", @@ -61,7 +61,7 @@ export const saveDashboardLayoutInBrowser = async ({ }): Promise => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { managed, diff --git a/packages/vitnode/src/views/admin/views/core/dashboard/widgets/resolve-widgets.ts b/packages/vitnode/src/views/admin/views/core/dashboard/widgets/resolve-widgets.ts index 7b3db2e10a..2bb185337e 100644 --- a/packages/vitnode/src/views/admin/views/core/dashboard/widgets/resolve-widgets.ts +++ b/packages/vitnode/src/views/admin/views/core/dashboard/widgets/resolve-widgets.ts @@ -3,7 +3,7 @@ import type { AdminDashboardWidget } from "@/lib/plugin"; import type { VitNodeConfig } from "@/vitnode.config"; import { hasStaffPermission } from "@/api/lib/staff-permission"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import type { ResolvedDashboardWidget } from "./types"; @@ -98,7 +98,7 @@ export const dashboardWidgetSources = ({ pluginTitle: (pluginId: string) => string; }): DashboardWidgetSource[] => [ { - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, keyPrefix: "admin.dashboard.widgets", pluginTitle: coreTitle, widgets: coreWidgets, diff --git a/packages/vitnode/src/views/admin/views/core/dashboard/widgets/widget-mutations.ts b/packages/vitnode/src/views/admin/views/core/dashboard/widgets/widget-mutations.ts index 64eb4e6cae..455cdbaf62 100644 --- a/packages/vitnode/src/views/admin/views/core/dashboard/widgets/widget-mutations.ts +++ b/packages/vitnode/src/views/admin/views/core/dashboard/widgets/widget-mutations.ts @@ -2,8 +2,8 @@ import type { z } from "zod"; import type { zodSendNotificationSchema } from "@/api/modules/admin/routes/notifications.route"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; import type { DashboardMutationResult } from "./dashboard-actions"; @@ -17,7 +17,7 @@ export const saveWidgetSettingsInBrowser = async ({ }): Promise => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { settings, widgetId } }, method: "put", module: "admin/dashboard", @@ -41,7 +41,7 @@ export const sendNotificationInBrowser = async ( ): Promise => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body }, method: "post", module: "admin", diff --git a/packages/vitnode/src/views/admin/views/core/debug/debug-query.ts b/packages/vitnode/src/views/admin/views/core/debug/debug-query.ts index 8707005e49..4eb3eee06e 100644 --- a/packages/vitnode/src/views/admin/views/core/debug/debug-query.ts +++ b/packages/vitnode/src/views/admin/views/core/debug/debug-query.ts @@ -7,8 +7,8 @@ import type { } from "@/views/admin/table/params"; import type { QueueStatus } from "@/views/admin/views/core/advanced/queue/queue-query"; -import { CONFIG_PLUGIN } from "@/config"; import { OPERATIONAL_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { AdminRequestError, @@ -53,7 +53,7 @@ export type DebugLogsPageFetcher = ( /** One page of the log, over whichever transport the host hands in. */ export const fetchDebugLogsPage: DebugLogsPageFetcher = async params => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: params }, method: "get", module: "admin/debug", @@ -125,7 +125,7 @@ export type DebugQueueFetcher = () => Promise; /** The snapshot, over whichever transport the host hands in. */ export const fetchDebugQueue: DebugQueueFetcher = async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "admin/debug", path: "/queue", diff --git a/packages/vitnode/src/views/admin/views/core/navigation/navigation-mutations.ts b/packages/vitnode/src/views/admin/views/core/navigation/navigation-mutations.ts index d30153a51b..bc24c5ac10 100644 --- a/packages/vitnode/src/views/admin/views/core/navigation/navigation-mutations.ts +++ b/packages/vitnode/src/views/admin/views/core/navigation/navigation-mutations.ts @@ -6,8 +6,8 @@ import type { zodUpdateNavigationSchema, } from "@/api/modules/admin/navigation/lib/schema"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; import { type AdminMutationResult, runAdminApiMutation, @@ -33,7 +33,7 @@ export const createAdminNavigation = async ( parse: async response => (await response.json()) as { id: number }, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body }, method: "post", module: "admin/navigation", @@ -51,7 +51,7 @@ export const updateAdminNavigation = async ( parse: () => true as const, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body, params: { id: String(id) } }, method: "patch", module: "admin/navigation", @@ -68,7 +68,7 @@ export const reorderAdminNavigation = async ( parse: () => true as const, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body }, method: "post", module: "admin/navigation", @@ -85,7 +85,7 @@ export const deleteAdminNavigation = async ( parse: () => true as const, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id: String(id) } }, method: "delete", module: "admin/navigation", diff --git a/packages/vitnode/src/views/admin/views/core/navigation/navigation-query.ts b/packages/vitnode/src/views/admin/views/core/navigation/navigation-query.ts index 25498d0c12..358559b9c0 100644 --- a/packages/vitnode/src/views/admin/views/core/navigation/navigation-query.ts +++ b/packages/vitnode/src/views/admin/views/core/navigation/navigation-query.ts @@ -8,8 +8,8 @@ import type { } from "@/lib/navigation"; import type { AdminIdentity } from "@/views/admin/views/core/shared/admin-scope"; -import { CONFIG_PLUGIN } from "@/config"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { AdminRequestError } from "@/views/admin/admin-request"; import { @@ -46,7 +46,7 @@ export interface AdminNavigationPresets { export const fetchAdminNavigation = async (): Promise => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "admin/navigation", path: "/list", @@ -62,7 +62,7 @@ export const fetchAdminNavigation = async (): Promise => { export const fetchAdminNavigationPresets = async (): Promise => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "admin/navigation", path: "/presets", diff --git a/packages/vitnode/src/views/admin/views/core/shared/admin-permission-parity.test.ts b/packages/vitnode/src/views/admin/views/core/shared/admin-permission-parity.test.ts index 47162648eb..4bb8dd32f7 100644 --- a/packages/vitnode/src/views/admin/views/core/shared/admin-permission-parity.test.ts +++ b/packages/vitnode/src/views/admin/views/core/shared/admin-permission-parity.test.ts @@ -3,7 +3,7 @@ import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import { ADMIN_ROLE_PERMISSIONS, @@ -116,7 +116,7 @@ describe("the roles screen names the API's own permissions", () => { expect(ADMIN_ROLE_PERMISSIONS.view).toEqual({ module: "roles", permission: "can_view", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, }); }); @@ -199,6 +199,6 @@ describe("every tuple is a core permission", () => { ])("%s names @vitnode/core", (_name, permission) => { // A permission granted under core must not open a plugin's page, and the // reverse: a core screen naming a plugin id would check a grant nobody has. - expect(permission.plugin).toBe(CONFIG_PLUGIN.pluginId); + expect(permission.plugin).toBe(manifest.id); }); }); diff --git a/packages/vitnode/src/views/admin/views/core/shared/admin-permissions.ts b/packages/vitnode/src/views/admin/views/core/shared/admin-permissions.ts index 1c4820dc1a..41206a0f18 100644 --- a/packages/vitnode/src/views/admin/views/core/shared/admin-permissions.ts +++ b/packages/vitnode/src/views/admin/views/core/shared/admin-permissions.ts @@ -3,13 +3,13 @@ import type { PermissionStaffType, } from "@/api/lib/permission-staff"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; /** A core permission tuple. The plugin is always `@vitnode/core` here. */ const core = (module: string, permission: string): PermissionsStaffArgs => ({ module, permission, - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, }); export const ADMIN_USER_PERMISSIONS = { diff --git a/packages/vitnode/src/views/admin/views/core/staff/edit/scope/scope-query.ts b/packages/vitnode/src/views/admin/views/core/staff/edit/scope/scope-query.ts index 0a9b9b7b3c..cfd8e8fc7d 100644 --- a/packages/vitnode/src/views/admin/views/core/staff/edit/scope/scope-query.ts +++ b/packages/vitnode/src/views/admin/views/core/staff/edit/scope/scope-query.ts @@ -1,6 +1,6 @@ import { infiniteQueryOptions, queryOptions } from "@tanstack/react-query"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { AdminRequestError } from "@/views/admin/admin-request"; @@ -28,7 +28,7 @@ const fetchScopeResources = async ({ search?: string; }): Promise => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { family }, query: { diff --git a/packages/vitnode/src/views/admin/views/core/staff/staff-mutations.ts b/packages/vitnode/src/views/admin/views/core/staff/staff-mutations.ts index 7159036463..76e77e7e0c 100644 --- a/packages/vitnode/src/views/admin/views/core/staff/staff-mutations.ts +++ b/packages/vitnode/src/views/admin/views/core/staff/staff-mutations.ts @@ -3,8 +3,8 @@ import type { PermissionStaffType, } from "@/api/lib/permission-staff"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; import { type AdminMutationResult, runAdminApiMutation, @@ -26,7 +26,7 @@ export const createStaffEntry = async ({ parse: async response => (await response.json()) as { id: number }, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { roleId, userId }, params: { type } }, method: "post", module: "admin/staff", @@ -62,7 +62,7 @@ export const updateStaffPermissions = async ({ }, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { permissions, unrestricted }, params: { id, type }, @@ -87,7 +87,7 @@ export const deleteStaffEntry = async ({ parse: () => true as const, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id: String(id), type } }, method: "delete", module: "admin/staff", diff --git a/packages/vitnode/src/views/admin/views/core/staff/staff-query.ts b/packages/vitnode/src/views/admin/views/core/staff/staff-query.ts index 526b79ab76..c43f481f30 100644 --- a/packages/vitnode/src/views/admin/views/core/staff/staff-query.ts +++ b/packages/vitnode/src/views/admin/views/core/staff/staff-query.ts @@ -13,6 +13,7 @@ import type { import type { AdminIdentity } from "@/views/admin/views/core/shared/admin-scope"; import type { StaffCatalog } from "@/views/admin/views/core/staff/staff-model"; +import { manifest } from "@/manifest"; import { AdminRequestError, describeAdminParams, @@ -51,7 +52,6 @@ export { adminStaffPermissions, staffPermissionModuleFor, } from "@/views/admin/views/core/shared/admin-permissions"; -import { CONFIG_PLUGIN } from "@/config"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; import { fetcher } from "@/tanstack/fetcher"; @@ -96,7 +96,7 @@ export const fetchAdminStaffPage: AdminStaffPageFetcher = async ( { signal } = {}, ) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: params }, method: "get", module: "admin/staff", @@ -159,7 +159,7 @@ export type AdminStaffCatalogFetcher = () => Promise; export const fetchAdminStaffCatalog: AdminStaffCatalogFetcher = async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "admin/staff", path: "/permission-catalog", @@ -217,7 +217,7 @@ export const fetchAdminStaffEntry: AdminStaffEntryFetcher = async ( id, ) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id, type } }, method: "get", module: "admin/staff", diff --git a/packages/vitnode/src/views/admin/views/core/system/files/actions/file-row-actions.tsx b/packages/vitnode/src/views/admin/views/core/system/files/actions/file-row-actions.tsx index da2cf9451e..a48c3f466b 100644 --- a/packages/vitnode/src/views/admin/views/core/system/files/actions/file-row-actions.tsx +++ b/packages/vitnode/src/views/admin/views/core/system/files/actions/file-row-actions.tsx @@ -23,8 +23,8 @@ import { } from "@/components/ui/dialog"; import { Spinner } from "@/components/ui/spinner"; import { TooltipWithContent } from "@/components/ui/tooltip"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; import type { DeleteAdminFile } from "../files-delete"; @@ -37,7 +37,7 @@ const FileAltContent = React.lazy(async () => const FILES_EDIT_ALT = { module: "files", permission: "can_edit_alt", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, } as const; const EditAltAction = ({ id, name }: { id: number; name: string }) => { @@ -99,7 +99,7 @@ export const FileRowActions = ({ setIsDownloading(true); try { const res = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, module: "admin/files", path: "/{id}/download", method: "get", diff --git a/packages/vitnode/src/views/admin/views/core/system/files/alt/file-alt-query.ts b/packages/vitnode/src/views/admin/views/core/system/files/alt/file-alt-query.ts index f8ba8883ae..c2815baea9 100644 --- a/packages/vitnode/src/views/admin/views/core/system/files/alt/file-alt-query.ts +++ b/packages/vitnode/src/views/admin/views/core/system/files/alt/file-alt-query.ts @@ -1,8 +1,8 @@ import { queryOptions } from "@tanstack/react-query"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { AdminRequestError } from "@/views/admin/admin-request"; import { adminQueryRoot } from "@/views/admin/table/query"; @@ -32,7 +32,7 @@ export type FileAltFetcher = (id: number) => Promise; export const fetchFileAlt: FileAltFetcher = async id => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id } }, method: "get", module: "admin/files", @@ -74,7 +74,7 @@ export const saveFileAlt = async ( parse: ok, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body, params: { id } }, method: "put", module: "admin/files", @@ -92,7 +92,7 @@ export const removeFileAlt = async ( parse: ok, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id }, query: { languageCode } }, method: "delete", module: "admin/files", @@ -110,7 +110,7 @@ export const saveFileAltPolicy = async ( parse: ok, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { altPolicy }, params: { id } }, method: "put", module: "admin/files", diff --git a/packages/vitnode/src/views/admin/views/core/system/files/files-delete.ts b/packages/vitnode/src/views/admin/views/core/system/files/files-delete.ts index 8524411314..f2b71d833c 100644 --- a/packages/vitnode/src/views/admin/views/core/system/files/files-delete.ts +++ b/packages/vitnode/src/views/admin/views/core/system/files/files-delete.ts @@ -1,10 +1,10 @@ import type { BulkDeleteFilesResult } from "@/lib/files/bulk-delete"; import type { DeleteFileResult } from "@/lib/files/in-use"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; import { runBulkFileDelete } from "@/lib/files/bulk-delete"; import { readFileInUse } from "@/lib/files/in-use"; +import { manifest } from "@/manifest"; /** Deleting one file. `force` releases retained revisions; see `FileInUse`. */ export interface DeleteAdminFileArgs { @@ -32,7 +32,7 @@ export const deleteAdminFileInBrowser: DeleteAdminFile = async ({ }) => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id: String(id) }, query: force ? { force: "true" } : {}, diff --git a/packages/vitnode/src/views/admin/views/core/system/files/files-query.ts b/packages/vitnode/src/views/admin/views/core/system/files/files-query.ts index 6d0c7a5c2c..4ac50432c6 100644 --- a/packages/vitnode/src/views/admin/views/core/system/files/files-query.ts +++ b/packages/vitnode/src/views/admin/views/core/system/files/files-query.ts @@ -6,8 +6,8 @@ import type { AdminTableParams, } from "@/views/admin/table/params"; -import { CONFIG_PLUGIN } from "@/config"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { AdminRequestError, @@ -66,7 +66,7 @@ export type AdminFilesPageFetcher = ( /** One page, over whichever transport the host hands in. */ export const fetchAdminFilesPage: AdminFilesPageFetcher = async params => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: params }, method: "get", module: "admin/files", diff --git a/packages/vitnode/src/views/admin/views/core/system/integrations/integrations-query.ts b/packages/vitnode/src/views/admin/views/core/system/integrations/integrations-query.ts index 7603b92043..5ba66ad9df 100644 --- a/packages/vitnode/src/views/admin/views/core/system/integrations/integrations-query.ts +++ b/packages/vitnode/src/views/admin/views/core/system/integrations/integrations-query.ts @@ -1,7 +1,7 @@ import { queryOptions } from "@tanstack/react-query"; -import { CONFIG_PLUGIN } from "@/config"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { AdminRequestError } from "@/views/admin/admin-request"; import { adminQueryRoot } from "@/views/admin/table/query"; @@ -47,7 +47,7 @@ export type IntegrationsFetcher = () => Promise; /** The board's data, over whichever transport the host hands in. */ export const fetchIntegrations: IntegrationsFetcher = async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "admin/debug", path: "/integrations", diff --git a/packages/vitnode/src/views/admin/views/core/system/integrations/send-test-email/send-test-email-mutation.ts b/packages/vitnode/src/views/admin/views/core/system/integrations/send-test-email/send-test-email-mutation.ts index a96869eaec..49e62f69cc 100644 --- a/packages/vitnode/src/views/admin/views/core/system/integrations/send-test-email/send-test-email-mutation.ts +++ b/packages/vitnode/src/views/admin/views/core/system/integrations/send-test-email/send-test-email-mutation.ts @@ -2,8 +2,8 @@ import type { z } from "zod"; import type { zodSendTestEmailSchema } from "@/api/modules/admin/debug/routes/send-test-email.route"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; export type SendTestEmailBody = z.infer; @@ -16,7 +16,7 @@ export type SendTestEmail = ( export const sendTestEmailInBrowser: SendTestEmail = async body => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body }, method: "post", module: "admin/debug", diff --git a/packages/vitnode/src/views/admin/views/core/system/integrations/test-ai/content.tsx b/packages/vitnode/src/views/admin/views/core/system/integrations/test-ai/content.tsx index 91ae7cfdc7..34de0baa33 100644 --- a/packages/vitnode/src/views/admin/views/core/system/integrations/test-ai/content.tsx +++ b/packages/vitnode/src/views/admin/views/core/system/integrations/test-ai/content.tsx @@ -13,8 +13,8 @@ import { SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; import type { TestAIModel } from "./test-ai"; @@ -54,7 +54,7 @@ export const ContentTestAI = ({ models }: { models: TestAIModel[] }) => { try { const res = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, module: "admin/debug", path: "/test-ai", method: "post", diff --git a/packages/vitnode/src/views/admin/views/core/system/integrations/test-storage/content.tsx b/packages/vitnode/src/views/admin/views/core/system/integrations/test-storage/content.tsx index a88c95b3b6..6d992101c8 100644 --- a/packages/vitnode/src/views/admin/views/core/system/integrations/test-storage/content.tsx +++ b/packages/vitnode/src/views/admin/views/core/system/integrations/test-storage/content.tsx @@ -20,8 +20,8 @@ import { AttachmentTitle, AttachmentTrigger, } from "@/components/ui/attachment"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; export const ContentTestStorage = () => { const t = useTranslations("admin.system.integrations.storage.test"); @@ -35,7 +35,7 @@ export const ContentTestStorage = () => { formData.append("file", file); const res = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, module: "admin/debug", path: "/test-storage-upload", method: "post", diff --git a/packages/vitnode/src/views/admin/views/core/system/notifications/notifications-mutations.ts b/packages/vitnode/src/views/admin/views/core/system/notifications/notifications-mutations.ts index 30c911e738..b6c0882e55 100644 --- a/packages/vitnode/src/views/admin/views/core/system/notifications/notifications-mutations.ts +++ b/packages/vitnode/src/views/admin/views/core/system/notifications/notifications-mutations.ts @@ -1,7 +1,7 @@ import type { NotificationEmailMode } from "@/lib/notifications/types"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; export type NotificationsMutationResult = | { data: T; error?: never; status?: never } @@ -54,7 +54,7 @@ export const notificationsAdminActionsInBrowser: NotificationsAdminActions = { cancelQueuedEmails: async () => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "admin/notifications", path: "/emails/cancel", @@ -69,7 +69,7 @@ export const notificationsAdminActionsInBrowser: NotificationsAdminActions = { deleteAll: async () => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "admin/notifications", path: "/delete-all", @@ -84,7 +84,7 @@ export const notificationsAdminActionsInBrowser: NotificationsAdminActions = { markEverythingRead: async () => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "admin/notifications", path: "/read-all", @@ -99,7 +99,7 @@ export const notificationsAdminActionsInBrowser: NotificationsAdminActions = { pause: async () => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "admin/notifications", path: "/pause", @@ -114,7 +114,7 @@ export const notificationsAdminActionsInBrowser: NotificationsAdminActions = { resetMemberPreferences: async () => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "admin/notifications", path: "/members/reset-preferences", @@ -129,7 +129,7 @@ export const notificationsAdminActionsInBrowser: NotificationsAdminActions = { resume: async () => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "admin/notifications", path: "/resume", @@ -144,7 +144,7 @@ export const notificationsAdminActionsInBrowser: NotificationsAdminActions = { sendTestEmail: async () => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "admin/notifications", path: "/test-email", @@ -159,7 +159,7 @@ export const notificationsAdminActionsInBrowser: NotificationsAdminActions = { updateTypePolicy: async (type, body) => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body, params: { type } }, method: "put", module: "admin/notifications", diff --git a/packages/vitnode/src/views/admin/views/core/system/notifications/notifications-query.ts b/packages/vitnode/src/views/admin/views/core/system/notifications/notifications-query.ts index 435b96c4c6..f0cb6d4977 100644 --- a/packages/vitnode/src/views/admin/views/core/system/notifications/notifications-query.ts +++ b/packages/vitnode/src/views/admin/views/core/system/notifications/notifications-query.ts @@ -2,8 +2,8 @@ import { queryOptions } from "@tanstack/react-query"; import type { NotificationEmailMode } from "@/lib/notifications/types"; -import { CONFIG_PLUGIN } from "@/config"; import { OPERATIONAL_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { AdminRequestError } from "@/views/admin/admin-request"; import { adminQueryRoot } from "@/views/admin/table/query"; @@ -86,7 +86,7 @@ export interface AdminNotificationStats { export const fetchNotificationsOverview = async (): Promise => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "admin/notifications", path: "/overview", @@ -110,7 +110,7 @@ export const fetchNotificationStats = async ({ timeZone: string; }): Promise => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: { range, timeZone } }, method: "get", module: "admin/notifications", diff --git a/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-icon.tsx b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-icon.tsx new file mode 100644 index 0000000000..54cc979417 --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-icon.tsx @@ -0,0 +1,25 @@ +import { PuzzleIcon } from "lucide-react"; + +import { DynamicIcon } from "@/components/ui/dynamic-icon"; + +export const PluginIcon = ({ icon }: { icon: null | string }) => { + const fallback = ; + + return ( + + ); +}; diff --git a/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-search.test.ts b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-search.test.ts new file mode 100644 index 0000000000..fa18d1924d --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-search.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; + +import { matchesPluginSearch } from "./plugin-search"; + +const blog = { + description: "Posts and Categories", + id: "@acme/blog", + title: "Journal", +}; + +describe("matchesPluginSearch", () => { + it.each([ + ["an empty search", "", true], + ["a blank search", " ", true], + ["the title, in any case", "JOUR", true], + ["the id", "acme/bl", true], + ["the description", "categories", true], + ["a padded needle", " journal ", true], + ["nothing", "forum", false], + ])("matches %s", (_, search, expected) => { + expect(matchesPluginSearch(blog, search)).toBe(expected); + }); + + it("searches a plugin without a description by its title and id", () => { + const plugin = { ...blog, description: null }; + + expect(matchesPluginSearch(plugin, "acme")).toBe(true); + expect(matchesPluginSearch(plugin, "posts")).toBe(false); + }); +}); diff --git a/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-search.ts b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-search.ts new file mode 100644 index 0000000000..b0f5d971c1 --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-search.ts @@ -0,0 +1,13 @@ +import type { AdminInstalledPlugin } from "./plugins-query"; + +export const matchesPluginSearch = ( + plugin: Pick, + search: string, +): boolean => { + const needle = search.trim().toLowerCase(); + if (needle === "") return true; + + return [plugin.title, plugin.id, plugin.description ?? ""].some(value => + value.toLowerCase().includes(needle), + ); +}; diff --git a/packages/vitnode/src/views/admin/views/core/system/plugins/plugins-content.test.tsx b/packages/vitnode/src/views/admin/views/core/system/plugins/plugins-content.test.tsx new file mode 100644 index 0000000000..86b2e42956 --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/system/plugins/plugins-content.test.tsx @@ -0,0 +1,127 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { act, fireEvent, render, screen } from "@testing-library/react"; +import { IntlProvider } from "use-intl"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { + AdminInstalledPlugin, + AdminInstalledPlugins, +} from "./plugins-query"; + +import { PluginsContent } from "./plugins-content"; + +const plugin = ( + overrides: Partial = {}, +): AdminInstalledPlugin => ({ + description: "Posts and categories", + icon: null, + id: "@acme/blog", + registrations: { + api: { version: "1.0.0" }, + frontend: { version: "1.0.0" }, + }, + title: "Blog", + version: "1.0.0", + versionMismatch: false, + ...overrides, +}); + +const forum = plugin({ + description: "Threads and replies", + id: "@acme/forum", + registrations: { + api: { version: "2.0.0" }, + frontend: { version: "1.0.0" }, + }, + title: "Forum", + version: "2.0.0", + versionMismatch: true, +}); + +const renderPlugins = (data: Partial = {}) => + render( + + + + + , + ); + +const search = async (value: string) => { + fireEvent.change(screen.getByRole("searchbox"), { target: { value } }); + await act(async () => { + await vi.advanceTimersByTimeAsync(500); + }); +}; + +afterEach(() => { + vi.useRealTimers(); +}); + +describe("PluginsContent", () => { + it("narrows the list as you search, and clears a search with no results", async () => { + vi.useFakeTimers(); + renderPlugins(); + + await search("threads"); + expect(screen.queryByText("Blog")).toBeNull(); + expect(screen.getByText("Forum")).toBeTruthy(); + + await search("nothing like this"); + expect(screen.queryByText("Forum")).toBeNull(); + expect( + screen.getByText("admin.system.plugins.no_results.title"), + ).toBeTruthy(); + + fireEvent.click( + screen.getByRole("button", { + name: "admin.system.plugins.no_results.clear", + }), + ); + expect(screen.getByText("Blog")).toBeTruthy(); + expect(screen.getByText("Forum")).toBeTruthy(); + }); + + it("says no plugins are installed when there are none", () => { + renderPlugins({ plugins: [] }); + + expect(screen.getByText("admin.system.plugins.empty.title")).toBeTruthy(); + expect( + screen.queryByText("admin.system.plugins.no_results.title"), + ).toBeNull(); + }); + + it("warns only about the plugin whose API and frontend versions differ", () => { + renderPlugins(); + + expect(screen.getAllByText("admin.system.plugins.mismatch")).toHaveLength( + 1, + ); + }); + + it("marks a plugin with no known version", () => { + renderPlugins({ plugins: [plugin({ version: null })] }); + + expect( + screen.getByText("admin.system.plugins.version_unknown"), + ).toBeTruthy(); + }); + + it("shows the default icon for a plugin that names none", () => { + const { container } = renderPlugins({ plugins: [plugin({ icon: null })] }); + + expect(screen.getByText("Blog")).toBeTruthy(); + expect(container.querySelector("svg.lucide-puzzle")).not.toBeNull(); + }); +}); diff --git a/packages/vitnode/src/views/admin/views/core/system/plugins/plugins-content.tsx b/packages/vitnode/src/views/admin/views/core/system/plugins/plugins-content.tsx new file mode 100644 index 0000000000..077c1502aa --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/system/plugins/plugins-content.tsx @@ -0,0 +1,159 @@ +import { TriangleAlertIcon } from "lucide-react"; +import React from "react"; +import { useTranslations } from "use-intl"; + +import type { DataTableNavigation } from "@/components/table/navigation"; + +import { ContentDataTable } from "@/components/table/content"; +import { DataTableNavigationProvider } from "@/components/table/navigation"; +import { + readTablePage, + readTablePageSize, + readTableSearch, + withTableSearch, +} from "@/components/table/url-state"; +import { Button } from "@/components/ui/button"; + +import type { + AdminInstalledPlugin, + AdminInstalledPlugins, +} from "./plugins-query"; + +import { PluginIcon } from "./plugin-icon"; +import { matchesPluginSearch } from "./plugin-search"; + +export interface PluginsContentProps { + data: AdminInstalledPlugins; +} + +const PluginVersion = ({ version }: { version: null | string }) => { + const t = useTranslations("admin.system.plugins"); + + if (version === null) { + return ( + + {t("version_unknown")} + + ); + } + + return {version}; +}; + +const PluginCell = ({ plugin }: { plugin: AdminInstalledPlugin }) => { + const t = useTranslations("admin.system.plugins"); + const { api, frontend } = plugin.registrations; + + return ( +
+ +
+ {plugin.title} + {plugin.title === plugin.id ? null : ( + + {plugin.id} + + )} + {plugin.description === null ? ( +

+ {t("no_description")} +

+ ) : ( +

+ {plugin.description} +

+ )} + {plugin.versionMismatch && api?.version && frontend?.version ? ( +

+ + {t("mismatch", { api: api.version, frontend: frontend.version })} +

+ ) : null} +
+
+ ); +}; + +export const PluginsContent = ({ data }: PluginsContentProps) => { + const t = useTranslations("admin.system.plugins"); + const [searchParams, setSearchParams] = React.useState( + () => new URLSearchParams(), + ); + const navigation = React.useMemo( + () => ({ + navigate: nextSearch => { + setSearchParams(new URLSearchParams(nextSearch)); + }, + searchParams, + }), + [searchParams], + ); + + const search = readTableSearch(searchParams); + const rows = data.plugins.filter(plugin => + matchesPluginSearch(plugin, search), + ); + const pageSize = readTablePageSize(searchParams); + const totalPages = Math.max(1, Math.ceil(rows.length / pageSize)); + const currentPage = Math.min(readTablePage(searchParams), totalPages); + const pageRows = rows.slice( + (currentPage - 1) * pageSize, + currentPage * pageSize, + ); + + return ( + + , + }, + { + id: "version", + header: t("list.version"), + cell: ({ row }) => , + }, + ]} + customNoResults={ + data.plugins.length === 0 + ? { description: t("empty.desc"), title: t("empty.title") } + : { + description: t("no_results.desc"), + footer: ( + + ), + title: t("no_results.title"), + } + } + edges={pageRows} + id="installed-plugins" + order={{ defaultOrder: { column: "id", order: "asc" } }} + pageInfo={{ + count: pageRows.length, + currentPage, + endCursor: null, + hasNextPage: currentPage < totalPages, + hasPreviousPage: currentPage > 1, + pageSize, + startCursor: null, + totalCount: rows.length, + totalPages, + }} + search + searchPlaceholder={t("search")} + /> + + ); +}; diff --git a/packages/vitnode/src/views/admin/views/core/system/plugins/plugins-query.ts b/packages/vitnode/src/views/admin/views/core/system/plugins/plugins-query.ts new file mode 100644 index 0000000000..2d26e736ac --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/system/plugins/plugins-query.ts @@ -0,0 +1,56 @@ +import { queryOptions } from "@tanstack/react-query"; + +import { manifest } from "@/manifest"; +import { fetcher } from "@/tanstack/fetcher"; +import { AdminRequestError } from "@/views/admin/admin-request"; +import { adminQueryRoot } from "@/views/admin/table/query"; + +export interface AdminPluginRegistration { + version: null | string; +} + +export interface AdminInstalledPlugin { + description: null | string; + icon: null | string; + id: string; + registrations: { + api: AdminPluginRegistration | null; + frontend: AdminPluginRegistration | null; + }; + title: string; + version: null | string; + versionMismatch: boolean; +} + +export interface AdminInstalledPlugins { + core: { version: string }; + frontendInventory: boolean; + plugins: AdminInstalledPlugin[]; +} + +export type InstalledPluginsFetcher = () => Promise; + +export const fetchInstalledPlugins: InstalledPluginsFetcher = async () => { + const response = await fetcher({ + plugin: manifest.id, + method: "get", + module: "admin/plugins", + path: "/", + }); + + if (!response.ok) { + throw new AdminRequestError(response.status, "the plugin catalog"); + } + + return await response.json(); +}; + +export const installedPluginsQueryKey = adminQueryRoot("plugins"); + +export const installedPluginsQueryOptions = () => + queryOptions({ + queryFn: async () => await fetchInstalledPlugins(), + queryKey: installedPluginsQueryKey, + retry: false, + staleTime: Infinity, + }); diff --git a/packages/vitnode/src/views/admin/views/core/users/detail/user-account-mutations.ts b/packages/vitnode/src/views/admin/views/core/users/detail/user-account-mutations.ts index 96aa3bcd50..48e3a260b4 100644 --- a/packages/vitnode/src/views/admin/views/core/users/detail/user-account-mutations.ts +++ b/packages/vitnode/src/views/admin/views/core/users/detail/user-account-mutations.ts @@ -1,8 +1,8 @@ import type { SsoProfileField } from "@/lib/sso-profile"; import type { NotificationPreferenceTypeView } from "@/views/notifications/notifications-query"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; import { type AdminMutationResult, runAdminApiMutation, @@ -23,7 +23,7 @@ export const setAdminUserPassword = async ( parse: ok, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { password }, params: { id: String(id) } }, method: "put", module: "admin/users", @@ -41,7 +41,7 @@ export const revokeAdminUserDevice = async ( parse: ok, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id: String(id), publicId: encodeURIComponent(publicId) }, }, @@ -60,7 +60,7 @@ export const revokeAdminUserDevices = async ( parse: ok, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id: String(id) } }, method: "delete", module: "admin/users", @@ -79,7 +79,7 @@ export const renameAdminUserPasskey = async ( parse: async response => (await response.json()) as AdminUserPasskey, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { name }, params: { id: String(id), passkeyId: String(passkeyId) }, @@ -100,7 +100,7 @@ export const deleteAdminUserPasskey = async ( parse: ok, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id: String(id), passkeyId: String(passkeyId) } }, method: "delete", module: "admin/users", @@ -118,7 +118,7 @@ export const disconnectAdminUserSso = async ( parse: ok, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id: String(id), @@ -146,7 +146,7 @@ export const updateAdminUserSsoPreferences = async ( parse: ok, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body, params: { id: String(id) } }, method: "put", module: "admin/users", @@ -170,7 +170,7 @@ export const updateAdminUserNotificationPreferences = async ( .types, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { types }, params: { id: String(id) } }, method: "put", module: "admin/users", diff --git a/packages/vitnode/src/views/admin/views/core/users/detail/user-account-query.ts b/packages/vitnode/src/views/admin/views/core/users/detail/user-account-query.ts index db9c123b91..727ce17131 100644 --- a/packages/vitnode/src/views/admin/views/core/users/detail/user-account-query.ts +++ b/packages/vitnode/src/views/admin/views/core/users/detail/user-account-query.ts @@ -4,8 +4,8 @@ import type { AdminIdentity } from "@/views/admin/views/core/shared/admin-scope" import type { SsoConnectionsApi } from "@/views/auth/settings/sso/sso-connections-query"; import type { NotificationPreferenceTypeView } from "@/views/notifications/notifications-query"; -import { CONFIG_PLUGIN } from "@/config"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { AdminRequestError } from "@/views/admin/admin-request"; @@ -60,7 +60,7 @@ export const fetchAdminUserDevices = async ( userId: number, ): Promise => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: params(userId) }, method: "get", module: "admin/users", @@ -81,7 +81,7 @@ export const fetchAdminUserPasskeys = async ( userId: number, ): Promise => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: params(userId) }, method: "get", module: "admin/users", @@ -102,7 +102,7 @@ export const fetchAdminUserSso = async ( userId: number, ): Promise => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: params(userId) }, method: "get", module: "admin/users", @@ -123,7 +123,7 @@ export const fetchAdminUserNotificationPreferences = async ( userId: number, ): Promise => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: params(userId) }, method: "get", module: "admin/users", diff --git a/packages/vitnode/src/views/admin/views/core/users/detail/user-query.ts b/packages/vitnode/src/views/admin/views/core/users/detail/user-query.ts index f3bb1f6e4f..0a9799f3d2 100644 --- a/packages/vitnode/src/views/admin/views/core/users/detail/user-query.ts +++ b/packages/vitnode/src/views/admin/views/core/users/detail/user-query.ts @@ -6,8 +6,8 @@ import type { AdminIdentity } from "@/views/admin/views/core/shared/admin-scope" import type { AdminUserRole } from "@/views/admin/views/core/users/list/users-query"; import { hasStaffPermission } from "@/api/lib/staff-permission"; -import { CONFIG_PLUGIN } from "@/config"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { AdminRequestError } from "@/views/admin/admin-request"; import { ADMIN_USER_PERMISSIONS } from "@/views/admin/views/core/shared/admin-permissions"; @@ -59,7 +59,7 @@ export type AdminUserFetcher = (id: string) => Promise; export const fetchAdminUser: AdminUserFetcher = async id => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id } }, method: "get", module: "admin/users", diff --git a/packages/vitnode/src/views/admin/views/core/users/detail/user-timeline-query.ts b/packages/vitnode/src/views/admin/views/core/users/detail/user-timeline-query.ts index 278918aeb1..630a8d24bb 100644 --- a/packages/vitnode/src/views/admin/views/core/users/detail/user-timeline-query.ts +++ b/packages/vitnode/src/views/admin/views/core/users/detail/user-timeline-query.ts @@ -2,8 +2,8 @@ import { infiniteQueryOptions } from "@tanstack/react-query"; import type { AdminIdentity } from "@/views/admin/views/core/shared/admin-scope"; -import { CONFIG_PLUGIN } from "@/config"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { AdminRequestError } from "@/views/admin/admin-request"; import { @@ -27,7 +27,7 @@ export const adminUserTimelineQueryOptions = ({ initialPageParam: SEARCH_FEED_FIRST_PAGE, queryFn: async ({ pageParam, signal }) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id: String(userId) }, query: searchTimelineQuery({ cursor: pageParam, locale }), diff --git a/packages/vitnode/src/views/admin/views/core/users/list/users-query.ts b/packages/vitnode/src/views/admin/views/core/users/list/users-query.ts index 73fc1ea7dc..cf5a5ad0cc 100644 --- a/packages/vitnode/src/views/admin/views/core/users/list/users-query.ts +++ b/packages/vitnode/src/views/admin/views/core/users/list/users-query.ts @@ -8,9 +8,9 @@ import type { } from "@/views/admin/table/params"; import type { AdminIdentity } from "@/views/admin/views/core/shared/admin-scope"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { AdminRequestError, @@ -120,7 +120,7 @@ export const fetchAdminUsersPage: AdminUsersPageFetcher = async ( { signal } = {}, ) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: params }, method: "get", module: "admin/users", @@ -204,7 +204,7 @@ export const searchAdminUsersInBrowser: AdminUserSearchOptions = async search => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: { first: String(ADMIN_USER_SEARCH_LIMIT), search } }, method: "get", module: "admin/users", diff --git a/packages/vitnode/src/views/admin/views/core/users/roles/roles-mutations.ts b/packages/vitnode/src/views/admin/views/core/users/roles/roles-mutations.ts index 18cf17bf3b..5a65e9a324 100644 --- a/packages/vitnode/src/views/admin/views/core/users/roles/roles-mutations.ts +++ b/packages/vitnode/src/views/admin/views/core/users/roles/roles-mutations.ts @@ -2,8 +2,8 @@ import type { z } from "zod"; import type { zodCreateRoleAdminSchema } from "@/api/modules/admin/roles/routes/create.route"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; import { type AdminMutationResult, runAdminApiMutation, @@ -19,7 +19,7 @@ export const createAdminRole = async ( parse: async response => (await response.json()) as { id: number }, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body }, method: "post", module: "admin/roles", @@ -37,7 +37,7 @@ export const updateAdminRole = async ( parse: () => true as const, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body, params: { id: String(id) } }, method: "patch", module: "admin/roles", @@ -67,7 +67,7 @@ export const deleteAdminRole = async (args: { parse: () => true as const, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: deleteAdminRoleArgs(args), method: "delete", module: "admin/roles", diff --git a/packages/vitnode/src/views/admin/views/core/users/roles/roles-query.ts b/packages/vitnode/src/views/admin/views/core/users/roles/roles-query.ts index bf59246ca9..034b08cc0f 100644 --- a/packages/vitnode/src/views/admin/views/core/users/roles/roles-query.ts +++ b/packages/vitnode/src/views/admin/views/core/users/roles/roles-query.ts @@ -8,9 +8,9 @@ import type { } from "@/views/admin/table/params"; import type { AdminIdentity } from "@/views/admin/views/core/shared/admin-scope"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { AdminRequestError, @@ -81,7 +81,7 @@ export type AdminRolesPageFetcher = ( export const fetchAdminRolesPage: AdminRolesPageFetcher = async params => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: params }, method: "get", module: "admin/roles", @@ -151,7 +151,7 @@ export const adminRoleOptionsFrom = ( export const searchAdminRolesInBrowser: AdminRoleSearch = async search => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: { first: String(ADMIN_ROLE_SEARCH_LIMIT), search } }, method: "get", module: "admin/roles", diff --git a/packages/vitnode/src/views/admin/views/core/users/users-mutations.ts b/packages/vitnode/src/views/admin/views/core/users/users-mutations.ts index 8984c101c7..7c0f930b07 100644 --- a/packages/vitnode/src/views/admin/views/core/users/users-mutations.ts +++ b/packages/vitnode/src/views/admin/views/core/users/users-mutations.ts @@ -1,8 +1,8 @@ import type { UserImageKind } from "@/lib/user-images"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; import { readApiErrorMessage } from "@/lib/read-api-error"; +import { manifest } from "@/manifest"; import { type AdminMutationResult, runAdminApiMutation, @@ -41,7 +41,7 @@ export const updateAdminUser = async ( parse: async response => (await response.json()) as AdminUserUpdated, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body, params: { id: String(id) } }, method: "patch", module: "admin/users", @@ -70,7 +70,7 @@ export const verifyAdminUserEmail = async ( parse: async response => (await response.json()) as AdminUserVerified, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id: String(id) } }, method: "post", module: "admin/users", @@ -99,7 +99,7 @@ export const createAdminUser = async ( parse: async response => (await response.json()) as AdminUserCreated, request: async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body }, method: "post", module: "admin/users", @@ -146,7 +146,7 @@ export const uploadAdminUserImage = async ( formData.append("file", file); const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id: String(id), kind } }, formData, method: "post", @@ -170,7 +170,7 @@ export const removeAdminUserImage = async ( kind: UserImageKind, ): Promise => { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id: String(id), kind } }, method: "delete", module: "admin/users", diff --git a/packages/vitnode/src/views/auth/settings/ai/ai-usage-query.ts b/packages/vitnode/src/views/auth/settings/ai/ai-usage-query.ts index 16a3bca622..1619e5dc58 100644 --- a/packages/vitnode/src/views/auth/settings/ai/ai-usage-query.ts +++ b/packages/vitnode/src/views/auth/settings/ai/ai-usage-query.ts @@ -1,7 +1,7 @@ import { infiniteQueryOptions, queryOptions } from "@tanstack/react-query"; -import { CONFIG_PLUGIN } from "@/config"; import { OPERATIONAL_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; export const AI_USAGE_NOTICES = [ @@ -70,7 +70,7 @@ export type AiUsageFetcher = () => Promise; export const fetchAiUsage: AiUsageFetcher = async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "ai", path: "/usage", @@ -95,7 +95,7 @@ export type AiHistoryFetcher = ( export const fetchAiHistory: AiHistoryFetcher = async (before, filter = {}) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: { ...filter, diff --git a/packages/vitnode/src/views/auth/settings/devices/devices-query.ts b/packages/vitnode/src/views/auth/settings/devices/devices-query.ts index 0c63e6fa61..81047676b1 100644 --- a/packages/vitnode/src/views/auth/settings/devices/devices-query.ts +++ b/packages/vitnode/src/views/auth/settings/devices/devices-query.ts @@ -1,7 +1,7 @@ import { queryOptions } from "@tanstack/react-query"; -import { CONFIG_PLUGIN } from "@/config"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; /** Which icon a row gets, and the only three values the API will send. */ @@ -51,7 +51,7 @@ export const isDevicesRequestError = ( export const fetchDevices: DevicesFetcher = async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "users", path: "/devices", diff --git a/packages/vitnode/src/views/auth/settings/devices/devices-revoke.ts b/packages/vitnode/src/views/auth/settings/devices/devices-revoke.ts index f3ee4dff09..7bfa34c742 100644 --- a/packages/vitnode/src/views/auth/settings/devices/devices-revoke.ts +++ b/packages/vitnode/src/views/auth/settings/devices/devices-revoke.ts @@ -1,5 +1,5 @@ -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; import type { Device } from "./devices-query"; @@ -37,7 +37,7 @@ export const revokeDeviceInBrowser: RevokeDevice = async ({ publicId }) => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { publicId } }, method: "delete", module: "users", diff --git a/packages/vitnode/src/views/auth/settings/overview/personal-update.ts b/packages/vitnode/src/views/auth/settings/overview/personal-update.ts index be7c449f1d..1ed65682ee 100644 --- a/packages/vitnode/src/views/auth/settings/overview/personal-update.ts +++ b/packages/vitnode/src/views/auth/settings/overview/personal-update.ts @@ -5,9 +5,9 @@ import type { UserPersonalInformation, } from "@/lib/user-personal-information"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; import { OPERATIONAL_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; export interface PersonalInfoPolicy { @@ -19,7 +19,7 @@ export type PersonalInfoPolicyFetcher = () => Promise; export const fetchPersonalInfoPolicy: PersonalInfoPolicyFetcher = async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "users", path: "/me/policy", @@ -60,7 +60,7 @@ export const updatePersonalInformationInBrowser: UpdatePersonalInformation = async input => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: input }, method: "patch", module: "users", diff --git a/packages/vitnode/src/views/auth/settings/overview/time-zone-update.ts b/packages/vitnode/src/views/auth/settings/overview/time-zone-update.ts index 7f7fd0ffc6..9b8b133574 100644 --- a/packages/vitnode/src/views/auth/settings/overview/time-zone-update.ts +++ b/packages/vitnode/src/views/auth/settings/overview/time-zone-update.ts @@ -1,5 +1,5 @@ -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; export interface UpdateTimeZoneResult { data?: true; @@ -13,7 +13,7 @@ export type UpdateTimeZone = ( export const updateTimeZoneInBrowser: UpdateTimeZone = async timeZone => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { timeZone } }, method: "put", module: "users", diff --git a/packages/vitnode/src/views/auth/settings/passkeys/passkeys-mutations.ts b/packages/vitnode/src/views/auth/settings/passkeys/passkeys-mutations.ts index 27afb6b86d..643be3a249 100644 --- a/packages/vitnode/src/views/auth/settings/passkeys/passkeys-mutations.ts +++ b/packages/vitnode/src/views/auth/settings/passkeys/passkeys-mutations.ts @@ -1,5 +1,5 @@ -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; import type { PasskeyCeremonyFailure } from "../../passkeys/webauthn"; import type { Passkey } from "./passkeys-query"; @@ -46,7 +46,7 @@ const failed = (failure: Failure) => ({ failure, ok: false as const }); export const addPasskeyInBrowser: AddPasskey = async () => { try { const optionsResponse = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "users/passkeys", options: CREDENTIALS, @@ -66,7 +66,7 @@ export const addPasskeyInBrowser: AddPasskey = async () => { if (!ceremony.ok) return failed(ceremony.failure); const verifyResponse = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { response: ceremony.response } }, method: "post", module: "users/passkeys", @@ -93,7 +93,7 @@ export const addPasskeyInBrowser: AddPasskey = async () => { export const renamePasskeyInBrowser: RenamePasskey = async ({ id, name }) => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { name }, params: { id: String(id) } }, method: "patch", module: "users/passkeys", @@ -113,7 +113,7 @@ export const renamePasskeyInBrowser: RenamePasskey = async ({ id, name }) => { export const deletePasskeyInBrowser: DeletePasskey = async ({ id }) => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id: String(id) } }, method: "delete", module: "users/passkeys", diff --git a/packages/vitnode/src/views/auth/settings/passkeys/passkeys-query.ts b/packages/vitnode/src/views/auth/settings/passkeys/passkeys-query.ts index 2b1422ecad..61365f8c0c 100644 --- a/packages/vitnode/src/views/auth/settings/passkeys/passkeys-query.ts +++ b/packages/vitnode/src/views/auth/settings/passkeys/passkeys-query.ts @@ -1,7 +1,7 @@ import { queryOptions } from "@tanstack/react-query"; -import { CONFIG_PLUGIN } from "@/config"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; export interface Passkey { @@ -39,7 +39,7 @@ export const isPasskeysRequestError = ( export const fetchPasskeys: PasskeysFetcher = async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "users/passkeys", path: "/", diff --git a/packages/vitnode/src/views/auth/settings/sso/sso-connections-mutations.ts b/packages/vitnode/src/views/auth/settings/sso/sso-connections-mutations.ts index 110cc09aff..ee97003fe0 100644 --- a/packages/vitnode/src/views/auth/settings/sso/sso-connections-mutations.ts +++ b/packages/vitnode/src/views/auth/settings/sso/sso-connections-mutations.ts @@ -1,7 +1,7 @@ import type { SsoConnectionIntent, SsoProfileField } from "@/lib/sso-profile"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; import type { SsoProfileSources } from "./sso-connections-query"; @@ -61,7 +61,7 @@ export const startSsoConnectionInBrowser: StartSsoConnection = async ({ }) => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { fields, intent }, params: providerParam(providerId) }, method: "post", module: "users/sso/connections", @@ -121,7 +121,7 @@ export const completeSsoConnectionInBrowser = async ({ }): Promise => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { code, state }, params: providerParam(providerId) }, method: "post", module: "users/sso/connections", @@ -163,7 +163,7 @@ export const disconnectSsoConnectionInBrowser: DisconnectSsoConnection = async ({ providerId }) => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: providerParam(providerId) }, method: "delete", module: "users/sso/connections", @@ -196,7 +196,7 @@ export const saveSsoPreferencesInBrowser: SaveSsoPreferences = async ({ }) => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { sources, sync } }, method: "put", module: "users/sso/connections", @@ -244,7 +244,7 @@ export const applySsoImportInBrowser: ApplySsoImport = async ({ }) => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { fields }, params: providerParam(providerId) }, method: "post", module: "users/sso/connections", @@ -278,7 +278,7 @@ export const discardSsoImportInBrowser: DiscardSsoImport = async ({ }) => { try { await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: providerParam(providerId) }, method: "delete", module: "users/sso/connections", diff --git a/packages/vitnode/src/views/auth/settings/sso/sso-connections-query.ts b/packages/vitnode/src/views/auth/settings/sso/sso-connections-query.ts index 24d42949eb..6e8ed6d8fb 100644 --- a/packages/vitnode/src/views/auth/settings/sso/sso-connections-query.ts +++ b/packages/vitnode/src/views/auth/settings/sso/sso-connections-query.ts @@ -2,8 +2,8 @@ import { queryOptions } from "@tanstack/react-query"; import type { SsoProfileField } from "@/lib/sso-profile"; -import { CONFIG_PLUGIN } from "@/config"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; export interface SsoConnection { @@ -75,7 +75,7 @@ export const isSsoConnectionsRequestError = ( export const fetchSsoConnections: SsoConnectionsFetcher = async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "users/sso/connections", path: "/", @@ -92,7 +92,7 @@ export const fetchSsoImportPreview: SsoImportPreviewFetcher = async ({ providerId, }) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { providerId: encodeURIComponent(providerId) } }, method: "get", module: "users/sso/connections", diff --git a/packages/vitnode/src/views/auth/settings/widgets/settings-page.ts b/packages/vitnode/src/views/auth/settings/widgets/settings-page.ts index ceb69b9a3a..f213f79a9c 100644 --- a/packages/vitnode/src/views/auth/settings/widgets/settings-page.ts +++ b/packages/vitnode/src/views/auth/settings/widgets/settings-page.ts @@ -1,5 +1,5 @@ -import { CONFIG_PLUGIN } from "@/config"; import { defineEditablePage } from "@/content/editor"; +import { manifest } from "@/manifest"; export const SETTINGS_PAGE_ID = "core:settings"; @@ -16,7 +16,7 @@ export const settingsPage = defineEditablePage({ permission: { module: "widgets", permission: "can_edit", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, }, zones: { diff --git a/packages/vitnode/src/views/files/actions/file-row-actions.tsx b/packages/vitnode/src/views/files/actions/file-row-actions.tsx index 8645799125..584188b73a 100644 --- a/packages/vitnode/src/views/files/actions/file-row-actions.tsx +++ b/packages/vitnode/src/views/files/actions/file-row-actions.tsx @@ -8,8 +8,8 @@ import type { FileInUse } from "@/lib/files/in-use"; import { ConfirmActionAlertDialog } from "@/components/confirm-action/confirm-action-alert-dialog"; import { Button } from "@/components/ui/button"; import { TooltipWithContent } from "@/components/ui/tooltip"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; import type { DeleteMyFile } from "../my-files-delete"; @@ -32,7 +32,7 @@ export const MyFileRowActions = ({ setIsDownloading(true); try { const res = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, module: "users/files", path: "/{id}/download", method: "get", diff --git a/packages/vitnode/src/views/files/my-files-delete.ts b/packages/vitnode/src/views/files/my-files-delete.ts index bbb9473167..3d893511ac 100644 --- a/packages/vitnode/src/views/files/my-files-delete.ts +++ b/packages/vitnode/src/views/files/my-files-delete.ts @@ -1,13 +1,13 @@ import type { BulkDeleteFilesResult } from "@/lib/files/bulk-delete"; import type { DeleteFileResult } from "@/lib/files/in-use"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; import { runBulkFileDelete, shouldRefreshAfterBulkDelete, } from "@/lib/files/bulk-delete"; import { readFileInUse } from "@/lib/files/in-use"; +import { manifest } from "@/manifest"; export interface DeleteMyFileArgs { force?: boolean; @@ -33,7 +33,7 @@ export const deleteMyFileInBrowser: DeleteMyFile = async ({ }) => { try { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id: String(id) }, query: force ? { force: "true" } : {}, diff --git a/packages/vitnode/src/views/files/my-files-query.ts b/packages/vitnode/src/views/files/my-files-query.ts index ea65e0705f..586e88b657 100644 --- a/packages/vitnode/src/views/files/my-files-query.ts +++ b/packages/vitnode/src/views/files/my-files-query.ts @@ -1,9 +1,9 @@ import { queryOptions } from "@tanstack/react-query"; import { DEFAULT_TABLE_PAGE_SIZE } from "@/components/table/url-state"; -import { CONFIG_PLUGIN } from "@/config"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; import { readFirstValue, readPageSize } from "@/lib/table-params"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; /** The columns the list route will sort by. Anything else is ignored. */ @@ -156,7 +156,7 @@ export const fetchMyFilesPage: MyFilesPageFetcher = async ( { signal } = {}, ) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: params }, method: "get", module: "users/files", diff --git a/packages/vitnode/src/views/moderation/approval-mutations.ts b/packages/vitnode/src/views/moderation/approval-mutations.ts index 02017030e0..d45fc5ce41 100644 --- a/packages/vitnode/src/views/moderation/approval-mutations.ts +++ b/packages/vitnode/src/views/moderation/approval-mutations.ts @@ -1,5 +1,5 @@ -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; const DECISION_FAILURES = [ "decided", @@ -59,7 +59,7 @@ export const approveSubmission = async ({ id }: { id: number }) => await settle( async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id } }, method: "post", module: "moderation", @@ -78,7 +78,7 @@ export const rejectSubmission = async ({ await settle( async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { reason }, params: { id } }, method: "post", module: "moderation", diff --git a/packages/vitnode/src/views/moderation/modcp/modcp-query.ts b/packages/vitnode/src/views/moderation/modcp/modcp-query.ts index 82051b4377..3037b0cfd2 100644 --- a/packages/vitnode/src/views/moderation/modcp/modcp-query.ts +++ b/packages/vitnode/src/views/moderation/modcp/modcp-query.ts @@ -2,7 +2,7 @@ import type { QueryClient } from "@tanstack/react-query"; import { infiniteQueryOptions, queryOptions } from "@tanstack/react-query"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; export interface ModcpReportsFilter { @@ -37,7 +37,7 @@ const MODCP_ROOT = ["vitnode", "moderation"] as const; const fetchReports = async (filter: ModcpReportsFilter) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: filter }, method: "get", module: "moderation", @@ -82,7 +82,7 @@ export const modcpBoardColumnQueryOptions = ( const fetchReport = async (id: number) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id } }, method: "get", module: "moderation", @@ -105,7 +105,7 @@ export const modcpReportQueryOptions = (id: number) => const fetchAssignees = async (id: number) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id } }, method: "get", module: "moderation", @@ -144,7 +144,7 @@ export interface ModcpApprovalsFilter { const fetchApprovals = async (filter: ModcpApprovalsFilter) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: filter }, method: "get", module: "moderation", @@ -169,7 +169,7 @@ export const modcpApprovalsQueryOptions = (filter: ModcpApprovalsFilter) => const fetchApprovalFilters = async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "moderation", path: "/approvals/filters", @@ -195,7 +195,7 @@ export const modcpApprovalFiltersQueryOptions = () => const fetchApproval = async (id: number) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id } }, method: "get", module: "moderation", diff --git a/packages/vitnode/src/views/moderation/moderation-mutations.ts b/packages/vitnode/src/views/moderation/moderation-mutations.ts index e9b7621351..83ba158a13 100644 --- a/packages/vitnode/src/views/moderation/moderation-mutations.ts +++ b/packages/vitnode/src/views/moderation/moderation-mutations.ts @@ -1,8 +1,8 @@ import type { RichTextDocument } from "@/content/rich-text/document"; import type { ModerationReportReason } from "@/lib/moderation/const"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; const REPORTED_FAILURES = [ "already_claimed", @@ -83,7 +83,7 @@ export const createReport = async (input: CreateReportInput) => await settle<{ id: number }>( async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: input }, method: "post", module: "moderation", @@ -109,7 +109,7 @@ export const claimReport = async ({ await settle( async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { expectedVersion }, params: { id } }, method: "post", module: "moderation", @@ -131,7 +131,7 @@ export const assignReport = async ({ await settle( async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { assigneeId, expectedVersion }, params: { id } }, method: "post", module: "moderation", @@ -151,7 +151,7 @@ export const addReportNote = async ({ await settle<{ ok: boolean }>( async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { body }, params: { id } }, method: "post", module: "moderation", @@ -173,7 +173,7 @@ export const editReportNote = async ({ await settle<{ ok: boolean }>( async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { body }, params: { id, noteId } }, method: "post", module: "moderation", @@ -193,7 +193,7 @@ export const deleteReportNote = async ({ await settle<{ ok: boolean }>( async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id, noteId } }, method: "post", module: "moderation", @@ -217,7 +217,7 @@ export const resolveReport = async ({ await settle( async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { expectedVersion, @@ -246,7 +246,7 @@ export const reopenReport = async ({ await settle( async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { expectedVersion, reason }, params: { id } }, method: "post", module: "moderation", @@ -268,7 +268,7 @@ export const runReportAction = async ({ await settle( async () => await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { expectedVersion }, params: { action, id } }, method: "post", module: "moderation", diff --git a/packages/vitnode/src/views/notifications/notifications-actions.ts b/packages/vitnode/src/views/notifications/notifications-actions.ts index 12c61f9bea..1074b5183b 100644 --- a/packages/vitnode/src/views/notifications/notifications-actions.ts +++ b/packages/vitnode/src/views/notifications/notifications-actions.ts @@ -3,8 +3,8 @@ import type { NotificationState, } from "@/lib/notifications/types"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; import { NotificationsRequestError } from "./notifications-query"; @@ -20,7 +20,7 @@ export const markNotificationReadInBrowser = async ({ throughSeq?: number; }): Promise => { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { throughSeq }, params: { id } }, method: "post", module: "notifications", @@ -36,7 +36,7 @@ export const markNotificationUnreadInBrowser = async ({ id: number; }): Promise => { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id } }, method: "post", module: "notifications", @@ -52,7 +52,7 @@ export const archiveNotificationInBrowser = async ({ id: number; }): Promise => { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { id } }, method: "post", module: "notifications", @@ -68,7 +68,7 @@ export const markAllNotificationsReadInBrowser = async ({ category?: string; } = {}): Promise => { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: { category } }, method: "post", module: "notifications", @@ -93,7 +93,7 @@ export const updateNotificationPreferencesInBrowser = async ( body: UpdateNotificationPreferencesBody, ): Promise => { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body }, method: "put", module: "notifications", diff --git a/packages/vitnode/src/views/notifications/notifications-query.ts b/packages/vitnode/src/views/notifications/notifications-query.ts index a5ee8557bb..de0b17319f 100644 --- a/packages/vitnode/src/views/notifications/notifications-query.ts +++ b/packages/vitnode/src/views/notifications/notifications-query.ts @@ -2,11 +2,11 @@ import { infiniteQueryOptions, queryOptions } from "@tanstack/react-query"; import type { NotificationEmailMode } from "@/lib/notifications/types"; -import { CONFIG_PLUGIN } from "@/config"; import { OPERATIONAL_STALE_TIME, RECORD_STALE_TIME, } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; export interface NotificationActorView { @@ -74,7 +74,7 @@ export const fetchNotificationsPage = async ({ signal?: AbortSignal; }): Promise => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: { category, @@ -141,7 +141,7 @@ export const recentNotificationsQueryOptions = ({ export const fetchNotificationState = async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "notifications", path: "/state", @@ -186,7 +186,7 @@ export const notificationPreferencesQueryOptions = ({ queryOptions({ queryFn: async (): Promise => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "notifications", path: "/preferences", diff --git a/packages/vitnode/src/views/profile/images/self-images-query.ts b/packages/vitnode/src/views/profile/images/self-images-query.ts index e921016fb4..8723197ca7 100644 --- a/packages/vitnode/src/views/profile/images/self-images-query.ts +++ b/packages/vitnode/src/views/profile/images/self-images-query.ts @@ -2,10 +2,10 @@ import { queryOptions } from "@tanstack/react-query"; import type { UserImageKind, UserImagePolicy } from "@/lib/user-images"; -import { CONFIG_PLUGIN } from "@/config"; import { fetcherClient } from "@/lib/fetcher-client"; import { OPERATIONAL_STALE_TIME } from "@/lib/query-freshness"; import { readApiErrorMessage } from "@/lib/read-api-error"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { PROFILE_QUERY_ROOT } from "../profile-query"; @@ -14,7 +14,7 @@ export type UserImagePolicyFetcher = () => Promise; export const fetchUserImagePolicy: UserImagePolicyFetcher = async () => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "users/images", path: "/policy", @@ -67,7 +67,7 @@ export const uploadOwnUserImage = async ( formData.append("file", file); const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { kind } }, formData, method: "post", @@ -89,7 +89,7 @@ export const removeOwnUserImage = async ( kind: UserImageKind, ): Promise => { const response = await fetcherClient({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { kind } }, method: "delete", module: "users/images", diff --git a/packages/vitnode/src/views/profile/profile-query.ts b/packages/vitnode/src/views/profile/profile-query.ts index 3756c1daab..7c81b7479f 100644 --- a/packages/vitnode/src/views/profile/profile-query.ts +++ b/packages/vitnode/src/views/profile/profile-query.ts @@ -2,8 +2,8 @@ import { queryOptions } from "@tanstack/react-query"; import type { RoleNameEntry } from "@/components/role-name"; -import { CONFIG_PLUGIN } from "@/config"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; export const MAX_NAME_CODE_LENGTH = 255; @@ -59,7 +59,7 @@ export const isProfileNotFound = (error: unknown): boolean => export const fetchUserProfile: UserProfileFetcher = async nameCode => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { nameCode: encodeURIComponent(nameCode) } }, method: "get", module: "users", diff --git a/packages/vitnode/src/views/profile/profile-timeline-query.ts b/packages/vitnode/src/views/profile/profile-timeline-query.ts index 0119289888..6e6746c17c 100644 --- a/packages/vitnode/src/views/profile/profile-timeline-query.ts +++ b/packages/vitnode/src/views/profile/profile-timeline-query.ts @@ -1,7 +1,7 @@ import { infiniteQueryOptions } from "@tanstack/react-query"; -import { CONFIG_PLUGIN } from "@/config"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { nextSearchFeedCursor, @@ -24,7 +24,7 @@ export const profileTimelineQueryOptions = ({ initialPageParam: SEARCH_FEED_FIRST_PAGE, queryFn: async ({ pageParam, signal }) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { params: { userId: String(userId) }, query: searchTimelineQuery({ cursor: pageParam, locale }), diff --git a/packages/vitnode/src/views/search/search-feed-query.ts b/packages/vitnode/src/views/search/search-feed-query.ts index 66f64d9616..48ca38a7e3 100644 --- a/packages/vitnode/src/views/search/search-feed-query.ts +++ b/packages/vitnode/src/views/search/search-feed-query.ts @@ -1,7 +1,7 @@ import { infiniteQueryOptions } from "@tanstack/react-query"; -import { CONFIG_PLUGIN } from "@/config"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import type { SearchFeedPage } from "./types"; @@ -142,7 +142,7 @@ export const fetchSearchFeedPage: SearchFeedPageFetcher = async ( { signal } = {}, ) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: searchFeedQuery(args) }, method: "get", module: "search", diff --git a/packages/vitnode/src/views/search/search-filters-query.ts b/packages/vitnode/src/views/search/search-filters-query.ts index d1acbb8cb1..ba8d79a960 100644 --- a/packages/vitnode/src/views/search/search-filters-query.ts +++ b/packages/vitnode/src/views/search/search-filters-query.ts @@ -1,7 +1,7 @@ import { keepPreviousData, queryOptions } from "@tanstack/react-query"; -import { CONFIG_PLUGIN } from "@/config"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import type { SearchFeedParams } from "./search-feed-query"; @@ -38,7 +38,7 @@ export const searchFacetsQueryOptions = ({ placeholderData: keepPreviousData, queryFn: async ({ signal }) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query }, method: "get", module: "search", @@ -64,7 +64,7 @@ export const searchAuthorsQueryOptions = ({ term }: { term: string }) => placeholderData: keepPreviousData, queryFn: async ({ signal }) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: term ? { search: term } : {} }, method: "get", module: "search", diff --git a/packages/vitnode/src/views/search/search-suggest-query.ts b/packages/vitnode/src/views/search/search-suggest-query.ts index bbf27dfa5f..107602f28c 100644 --- a/packages/vitnode/src/views/search/search-suggest-query.ts +++ b/packages/vitnode/src/views/search/search-suggest-query.ts @@ -1,7 +1,7 @@ import { queryOptions } from "@tanstack/react-query"; -import { CONFIG_PLUGIN } from "@/config"; import { RECORD_STALE_TIME } from "@/lib/query-freshness"; +import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; import { SEARCH_QUERY_ROOT } from "./search-feed-query"; @@ -24,7 +24,7 @@ export const searchSuggestQueryOptions = ({ queryOptions({ queryFn: async ({ signal }) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: { first: String(SEARCH_SUGGESTIONS_LIMIT), @@ -54,7 +54,7 @@ export const searchPopularQueryOptions = ({ locale }: { locale: string }) => queryOptions({ queryFn: async ({ signal }) => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { query: { first: String(SEARCH_SUGGESTIONS_LIMIT), lang: locale }, }, @@ -82,7 +82,7 @@ export const recordSearchPopularity = async (input: { signal: SearchPopularitySignal; }): Promise => { await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, args: { body: input }, method: "post", module: "search", diff --git a/packages/vitnode/src/vitnode.config.ts b/packages/vitnode/src/vitnode.config.ts index 6776d22d31..ac38276b8b 100644 --- a/packages/vitnode/src/vitnode.config.ts +++ b/packages/vitnode/src/vitnode.config.ts @@ -25,6 +25,7 @@ import type { } from "./lib/i18n/types"; import type { VitNodeMetadata } from "./lib/metadata"; import type { BuildPluginReturn } from "./lib/plugin"; +import type { FrontendPluginInventory } from "./lib/plugin-manifest"; import type { PersonalInformationFieldsConfig } from "./lib/user-personal-information"; import type { PageCacheConfig } from "./page-cache/config"; @@ -112,6 +113,8 @@ export interface VitNodeApiConfig { adapter?: EventsApiPlugin; }; + frontendPlugins?: FrontendPluginInventory; + i18n?: VitNodeApiI18nConfig; /** * Largest request body the API will read, in bytes. Defaults to 25 MB. diff --git a/packages/vitnode/src/ws/notifications.ts b/packages/vitnode/src/ws/notifications.ts index 58e1404772..f40793c346 100644 --- a/packages/vitnode/src/ws/notifications.ts +++ b/packages/vitnode/src/ws/notifications.ts @@ -1,6 +1,6 @@ import type { NotificationStateMessage } from "@/lib/notifications/types"; -import { CONFIG_PLUGIN } from "@/config"; +import { manifest } from "@/manifest"; import { createWebSocketChannel } from "./types"; @@ -19,7 +19,7 @@ export const notificationsChannel = createWebSocketChannel< >({ id: "inbox", module: "notifications", - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, }); export const notificationsStateChannel = createWebSocketChannel< @@ -28,5 +28,5 @@ export const notificationsStateChannel = createWebSocketChannel< >({ id: "state", module: "notifications", - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, }); diff --git a/packages/vitnode/vitest.config.ts b/packages/vitnode/vitest.config.ts index c5ccb77dfd..078911bad0 100644 --- a/packages/vitnode/vitest.config.ts +++ b/packages/vitnode/vitest.config.ts @@ -37,6 +37,7 @@ const INTEGRATION = [ "scripts/cli/commands/plugin.test.ts", "scripts/cli/commands/start.test.ts", "scripts/cli/dev/package-watch.test.ts", + "scripts/watch-plugin-manifest.test.ts", ]; export default defineConfig({ diff --git a/plugins/blog/package.json b/plugins/blog/package.json index f6e5c9755e..62727e9fa6 100644 --- a/plugins/blog/package.json +++ b/plugins/blog/package.json @@ -2,7 +2,11 @@ "name": "@vitnode/blog", "version": "2.0.0-canary.13", "description": "Blog plugin for VitNode, providing a blogging platform on Hono.js.", - "author": "VitNode Team", + "author": { + "name": "VitNode", + "email": "contact@vitnode.com", + "url": "https://vitnode.com" + }, "license": "MIT", "homepage": "https://vitnode.com", "repository": { diff --git a/plugins/blog/src/admin/content.tsx b/plugins/blog/src/admin/content.tsx index 335046e4e6..a1d12e96f4 100644 --- a/plugins/blog/src/admin/content.tsx +++ b/plugins/blog/src/admin/content.tsx @@ -2,7 +2,7 @@ import type { ContentFrontendPluginSource } from "@vitnode/core/lib/plugin"; import { contentTypeAdmin } from "@vitnode/core/lib/plugin"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { BlogArticleEditorField } from "@/views/admin/article/editor-field"; import { ArticleCoverAltField, @@ -17,7 +17,7 @@ import { BlogCategoryColorField } from "@/views/admin/category/color-field"; import { blogCategoryNav, blogPostNav } from "./nav"; export const adminContent = { - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, contentTypes: [ contentTypeAdmin({ ...blogPostNav, diff --git a/plugins/blog/src/admin/nav.tsx b/plugins/blog/src/admin/nav.tsx index c26d1af7bd..667577fd81 100644 --- a/plugins/blog/src/admin/nav.tsx +++ b/plugins/blog/src/admin/nav.tsx @@ -2,9 +2,9 @@ import type { AdminNavPluginSource } from "@vitnode/core/lib/plugin"; import { ListIcon, NotebookPenIcon } from "lucide-react"; -import { CONFIG_PLUGIN } from "@/const"; import { blogCategoryContentType } from "@/content/category"; import { blogPostContentType } from "@/content/post"; +import { manifest } from "@/manifest.gen"; /** The post content type, as the sidebar reads it. */ export const blogPostNav = { @@ -19,6 +19,6 @@ export const blogCategoryNav = { }; export const adminNav = { - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, contentTypes: [blogPostNav, blogCategoryNav], } satisfies AdminNavPluginSource; diff --git a/plugins/blog/src/api/ai/actions.ts b/plugins/blog/src/api/ai/actions.ts index d2fb1d4c48..c2f2538d29 100644 --- a/plugins/blog/src/api/ai/actions.ts +++ b/plugins/blog/src/api/ai/actions.ts @@ -15,7 +15,7 @@ import { excerptSource, unquote, } from "@/api/lib/ai-writing"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; const zodLocale = z.string().min(2).max(16); @@ -55,7 +55,7 @@ const canEditPosts = async ({ await checkStaffPermission(c, { module: "posts", permission: "can_edit", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, type: "admin", }); @@ -192,14 +192,11 @@ export const blogAiActions = [ ]; export const TRANSLATE_FIELD_AI_ACTION = aiActionRef( - CONFIG_PLUGIN.pluginId, + manifest.id, translateFieldAiAction, ); export const ARTICLE_REVIEW_AI_ACTION = aiActionRef( - CONFIG_PLUGIN.pluginId, + manifest.id, articleReviewAiAction, ); -export const EXCERPT_AI_ACTION = aiActionRef( - CONFIG_PLUGIN.pluginId, - excerptAiAction, -); +export const EXCERPT_AI_ACTION = aiActionRef(manifest.id, excerptAiAction); diff --git a/plugins/blog/src/api/builders.ts b/plugins/blog/src/api/builders.ts new file mode 100644 index 0000000000..1ab9de62d3 --- /dev/null +++ b/plugins/blog/src/api/builders.ts @@ -0,0 +1,8 @@ +import { bindApiPlugin } from "@vitnode/core/api/lib/bind-api-plugin"; +import { bindContentPlugin } from "@vitnode/core/content/server"; + +import { manifest } from "@/manifest.gen"; + +export const { buildModule, buildRoute } = bindApiPlugin(manifest); +export const { buildContentAdminModule, buildContentPublicModule } = + bindContentPlugin(manifest); diff --git a/plugins/blog/src/api/modules/admin/admin.module.ts b/plugins/blog/src/api/modules/admin/admin.module.ts index 5b41455c8f..803829df5b 100644 --- a/plugins/blog/src/api/modules/admin/admin.module.ts +++ b/plugins/blog/src/api/modules/admin/admin.module.ts @@ -1,21 +1,16 @@ -import { buildModule } from "@vitnode/core/api/lib/module"; -import { buildContentAdminModule } from "@vitnode/core/content/server"; - +import { buildContentAdminModule, buildModule } from "@/api/builders"; import { blogLegacyEventListeners } from "@/api/lib/events"; -import { CONFIG_PLUGIN } from "@/const"; import { categoryContent } from "@/database/categories"; import { postContent } from "@/database/posts"; import { aiAdminModule } from "./ai/ai.admin.module"; export const adminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "admin", routes: [], modules: [ aiAdminModule, buildContentAdminModule({ - pluginId: CONFIG_PLUGIN.pluginId, contentTypes: [categoryContent, postContent], }), ], diff --git a/plugins/blog/src/api/modules/admin/ai/ai.admin.module.ts b/plugins/blog/src/api/modules/admin/ai/ai.admin.module.ts index 8ae1f911f2..242bdb38cb 100644 --- a/plugins/blog/src/api/modules/admin/ai/ai.admin.module.ts +++ b/plugins/blog/src/api/modules/admin/ai/ai.admin.module.ts @@ -1,12 +1,9 @@ -import { buildModule } from "@vitnode/core/api/lib/module"; - -import { CONFIG_PLUGIN } from "@/const"; +import { buildModule } from "@/api/builders"; import { excerptAiAdminRoute } from "./routes/excerpt.route"; import { translateAiAdminRoute } from "./routes/translate.route"; export const aiAdminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "ai", routes: [translateAiAdminRoute, excerptAiAdminRoute], }); diff --git a/plugins/blog/src/api/modules/admin/ai/routes/excerpt.route.ts b/plugins/blog/src/api/modules/admin/ai/routes/excerpt.route.ts index 85b2ec9a9f..fca1be5496 100644 --- a/plugins/blog/src/api/modules/admin/ai/routes/excerpt.route.ts +++ b/plugins/blog/src/api/modules/admin/ai/routes/excerpt.route.ts @@ -1,11 +1,9 @@ -import { buildRoute } from "@vitnode/core/api/lib/route"; import { z } from "zod"; import { EXCERPT_AI_ACTION, zodExcerptAiSchema } from "@/api/ai/actions"; -import { CONFIG_PLUGIN } from "@/const"; +import { buildRoute } from "@/api/builders"; export const excerptAiAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "posts", permission: "can_edit" }, route: { method: "post", diff --git a/plugins/blog/src/api/modules/admin/ai/routes/translate.route.ts b/plugins/blog/src/api/modules/admin/ai/routes/translate.route.ts index 25966908c7..bc42504e6e 100644 --- a/plugins/blog/src/api/modules/admin/ai/routes/translate.route.ts +++ b/plugins/blog/src/api/modules/admin/ai/routes/translate.route.ts @@ -1,14 +1,12 @@ -import { buildRoute } from "@vitnode/core/api/lib/route"; import { z } from "zod"; import { TRANSLATE_FIELD_AI_ACTION, zodTranslateAiSchema, } from "@/api/ai/actions"; -import { CONFIG_PLUGIN } from "@/const"; +import { buildRoute } from "@/api/builders"; export const translateAiAdminRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, adminStaffPermission: { module: "posts", permission: "can_edit" }, route: { method: "post", diff --git a/plugins/blog/src/config.api.ts b/plugins/blog/src/config.api.ts index 743c253b75..1fe2234b96 100644 --- a/plugins/blog/src/config.api.ts +++ b/plugins/blog/src/config.api.ts @@ -1,22 +1,21 @@ import type { ApiPluginContract } from "@vitnode/core/api/lib/plugin"; import { buildApiPlugin } from "@vitnode/core/api/lib/plugin"; -import { buildContentPublicModule } from "@vitnode/core/content/server"; import { blogAiActions } from "@/api/ai/actions"; +import { buildContentPublicModule } from "@/api/builders"; import { adminModule } from "@/api/modules/admin/admin.module"; -import { CONFIG_PLUGIN } from "@/const"; import { categoryContent } from "@/database/categories"; import { postContent } from "@/database/posts"; +import { manifest } from "@/manifest.gen"; export const blogApiPlugin = () => buildApiPlugin({ - pluginId: CONFIG_PLUGIN.pluginId, + manifest, aiActions: blogAiActions, modules: [ adminModule, buildContentPublicModule({ - pluginId: CONFIG_PLUGIN.pluginId, // Skips any content type without `publicApi`, so the category // contributes nothing - it has no public URL of its own. contentTypes: [categoryContent, postContent], diff --git a/plugins/blog/src/config.tsx b/plugins/blog/src/config.tsx index 3156fc605a..d172abfaa4 100644 --- a/plugins/blog/src/config.tsx +++ b/plugins/blog/src/config.tsx @@ -1,13 +1,12 @@ import { buildPlugin } from "@vitnode/core/lib/plugin"; -import { CONFIG_PLUGIN } from "@/const"; - import messages from "./locales"; +import { manifest } from "./manifest.gen"; import { routes } from "./routes"; export const blogPlugin = () => buildPlugin({ - ...CONFIG_PLUGIN, + manifest, localeFiles: { en: "@vitnode/blog/locales/en.json", }, diff --git a/plugins/blog/src/const.ts b/plugins/blog/src/const.ts deleted file mode 100644 index 5195feb764..0000000000 --- a/plugins/blog/src/const.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const CONFIG_PLUGIN = { - pluginId: "@vitnode/blog" as const, -}; diff --git a/plugins/blog/src/manifest.gen.ts b/plugins/blog/src/manifest.gen.ts new file mode 100644 index 0000000000..f4b123381d --- /dev/null +++ b/plugins/blog/src/manifest.gen.ts @@ -0,0 +1,24 @@ +/* oxlint-disable */ + +// This file is generated by VitNode. Do not edit it, and do not format it. +// +// It is rewritten by `vitnode build` and `vitnode dev` from this package's +// package.json - `name`, `version`, `description`, `author` and `license` - and the title and +// icon in `src/plugin-info.ts`. Pass `manifest` to `buildPlugin`, +// `buildApiPlugin` and `bindApiPlugin` - it is where the plugin id lives. + +import { pluginInfo } from "./plugin-info" + +export const manifest = { + id: "@vitnode/blog", + title: pluginInfo.title ?? "@vitnode/blog", + icon: pluginInfo.icon ?? null, + description: "Blog plugin for VitNode, providing a blogging platform on Hono.js.", + author: { + name: "VitNode", + email: "contact@vitnode.com", + url: "https://vitnode.com", + }, + license: "MIT", + version: "2.0.0-canary.13", +} as const diff --git a/plugins/blog/src/pages/post-page.tsx b/plugins/blog/src/pages/post-page.tsx index fd6ab643c0..4bbb678616 100644 --- a/plugins/blog/src/pages/post-page.tsx +++ b/plugins/blog/src/pages/post-page.tsx @@ -21,8 +21,8 @@ import { useRecordView } from "@vitnode/core/tanstack/search"; import { EyeOffIcon } from "lucide-react"; import { useTranslations } from "use-intl"; -import { CONFIG_PLUGIN } from "@/const"; import { blogPostContentType } from "@/content/post"; +import { manifest } from "@/manifest.gen"; import { blogPostQueryOptions } from "./post-query"; @@ -78,7 +78,7 @@ const BlogPostPage = ({ { const [resolution, detail] = await Promise.all([ fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/blog", path: "/delivery/resolve/{slug}", @@ -47,7 +47,7 @@ const fetchBlogPost = async ({ }, }), fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/blog", path: "/{slug}", diff --git a/plugins/blog/src/plugin-info.ts b/plugins/blog/src/plugin-info.ts new file mode 100644 index 0000000000..293aaa6fb3 --- /dev/null +++ b/plugins/blog/src/plugin-info.ts @@ -0,0 +1,6 @@ +import type { PluginInfo } from "@vitnode/core/lib/plugin-manifest"; + +export const pluginInfo: PluginInfo = { + title: "Blog", + icon: "newspaper", +}; diff --git a/plugins/blog/src/plugin-metadata.gen.ts b/plugins/blog/src/plugin-metadata.gen.ts new file mode 100644 index 0000000000..270c41d7d3 --- /dev/null +++ b/plugins/blog/src/plugin-metadata.gen.ts @@ -0,0 +1,16 @@ +/* oxlint-disable */ + +// This file is generated by VitNode. Do not edit it, and do not format it. +// +// It is rewritten by `vitnode build` and `vitnode dev` from this package's +// package.json - `name`, `version` and `description` - and the title and +// icon in `src/plugin-metadata.ts`. Register `pluginMetadata` as `metadata` +// in both `buildPlugin` and `buildApiPlugin`. + +export const pluginMetadata = { + id: "@vitnode/blog", + title: "@vitnode/blog", + icon: null, + description: "Blog plugin for VitNode, providing a blogging platform on Hono.js.", + version: "2.0.0-canary.13", +} as const diff --git a/plugins/blog/src/views/admin/article/editor/ai-review.tsx b/plugins/blog/src/views/admin/article/editor/ai-review.tsx index d7bb468798..cf25c6a841 100644 --- a/plugins/blog/src/views/admin/article/editor/ai-review.tsx +++ b/plugins/blog/src/views/admin/article/editor/ai-review.tsx @@ -14,11 +14,11 @@ import { CheckIcon, SparklesIcon, WandSparklesIcon } from "lucide-react"; import React from "react"; import { useTranslations } from "use-intl"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { applyPassageFix, type PassageFix } from "./passage-fix"; -const ARTICLE_REVIEW_ACTION = `${CONFIG_PLUGIN.pluginId}:article.review`; +const ARTICLE_REVIEW_ACTION = `${manifest.id}:article.review`; interface ArticleReviewSuggestion { area: "clarity" | "completeness" | "structure" | "tone"; diff --git a/plugins/blog/src/views/admin/article/editor/ai.ts b/plugins/blog/src/views/admin/article/editor/ai.ts index 8f74c69aaf..70b6d8e2fb 100644 --- a/plugins/blog/src/views/admin/article/editor/ai.ts +++ b/plugins/blog/src/views/admin/article/editor/ai.ts @@ -8,7 +8,7 @@ import { } from "@vitnode/core/lib/ai/assist-client"; import { fetcher } from "@vitnode/core/tanstack/fetcher"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; export class ArticleAiError extends Error { constructor(status: number) { @@ -26,7 +26,7 @@ export const translateArticleText = async (body: { to: string; }): Promise => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "admin/ai", path: "/translate", @@ -51,7 +51,7 @@ export const translateArticleContent = async ({ to: string; }): Promise => { const response = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "admin/ai", path: "/translate", @@ -71,7 +71,7 @@ export const writeArticleExcerpt = async (input: { }): Promise => { try { const { output } = await requestAiAssist({ - action: `${CONFIG_PLUGIN.pluginId}:excerpt.generate`, + action: `${manifest.id}:excerpt.generate`, input, }); if (typeof output !== "string") throw new ArticleAiError(502); diff --git a/plugins/example/package.json b/plugins/example/package.json index 8090218522..5e557fcd80 100644 --- a/plugins/example/package.json +++ b/plugins/example/package.json @@ -2,6 +2,11 @@ "name": "@vitnode/example", "version": "2.0.0-canary.13", "description": "Reference plugin exercising the VitNode Content Engine end to end.", + "author": { + "name": "VitNode", + "email": "contact@vitnode.com", + "url": "https://vitnode.com" + }, "license": "MIT", "private": true, "type": "module", diff --git a/plugins/example/src/admin/content.tsx b/plugins/example/src/admin/content.tsx index e2c9c32b22..54c3d6d1a5 100644 --- a/plugins/example/src/admin/content.tsx +++ b/plugins/example/src/admin/content.tsx @@ -1,6 +1,6 @@ import type { ContentFrontendPluginSource } from "@vitnode/core/lib/plugin"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { exampleArticleNav, @@ -13,7 +13,7 @@ import { } from "./nav"; export const adminContent = { - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, containers: [exampleBoardsNav], contentTypes: [ exampleArticleNav, diff --git a/plugins/example/src/admin/nav.tsx b/plugins/example/src/admin/nav.tsx index a4e24cf8ac..a9edbc91e9 100644 --- a/plugins/example/src/admin/nav.tsx +++ b/plugins/example/src/admin/nav.tsx @@ -11,7 +11,6 @@ import { TagIcon, } from "lucide-react"; -import { CONFIG_PLUGIN } from "@/const"; import { articleContentType } from "@/content/article"; import { boardContainers } from "@/content/boards"; import { categoryContentType } from "@/content/category"; @@ -19,6 +18,7 @@ import { discussionContentType } from "@/content/discussion"; import { eventContentType } from "@/content/event"; import { pageContentType } from "@/content/page"; import { tagContentType } from "@/content/tag"; +import { manifest } from "@/manifest.gen"; /** The article content type, as the sidebar reads it. */ export const exampleArticleNav = { @@ -58,7 +58,7 @@ export const exampleBoardsNav = { }; export const adminNav = { - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, containers: [exampleBoardsNav], contentTypes: [ exampleArticleNav, diff --git a/plugins/example/src/api/ai/actions.ts b/plugins/example/src/api/ai/actions.ts index 2f5323991c..edc2635bc4 100644 --- a/plugins/example/src/api/ai/actions.ts +++ b/plugins/example/src/api/ai/actions.ts @@ -3,15 +3,15 @@ import { languageName } from "@vitnode/core/api/lib/ai/language-name"; import { checkStaffPermission } from "@vitnode/core/api/lib/check-staff-permission"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; import { articleContentType } from "@/content/article"; +import { manifest } from "@/manifest.gen"; export const articleExcerptAiAction = defineAiAction({ authorize: async ({ c }) => await checkStaffPermission(c, { module: articleContentType.permissionModule, permission: "can_edit", - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, type: "admin", }), buildPrompt: (input, { instructions }) => ({ diff --git a/plugins/example/src/api/builders.ts b/plugins/example/src/api/builders.ts new file mode 100644 index 0000000000..1ab9de62d3 --- /dev/null +++ b/plugins/example/src/api/builders.ts @@ -0,0 +1,8 @@ +import { bindApiPlugin } from "@vitnode/core/api/lib/bind-api-plugin"; +import { bindContentPlugin } from "@vitnode/core/content/server"; + +import { manifest } from "@/manifest.gen"; + +export const { buildModule, buildRoute } = bindApiPlugin(manifest); +export const { buildContentAdminModule, buildContentPublicModule } = + bindContentPlugin(manifest); diff --git a/plugins/example/src/api/modules/admin/admin.module.ts b/plugins/example/src/api/modules/admin/admin.module.ts index 4466c5b2cb..60321a240b 100644 --- a/plugins/example/src/api/modules/admin/admin.module.ts +++ b/plugins/example/src/api/modules/admin/admin.module.ts @@ -1,7 +1,4 @@ -import { buildModule } from "@vitnode/core/api/lib/module"; -import { buildContentAdminModule } from "@vitnode/core/content/server"; - -import { CONFIG_PLUGIN } from "@/const"; +import { buildContentAdminModule, buildModule } from "@/api/builders"; import { boardContainers } from "@/content/boards"; import { advancedArticleContent } from "@/database/advanced-articles"; import { articleContent } from "@/database/articles"; @@ -13,12 +10,10 @@ import { pageContent } from "@/database/pages"; import { tagContent } from "@/database/tags"; export const adminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "admin", routes: [], modules: [ buildContentAdminModule({ - pluginId: CONFIG_PLUGIN.pluginId, containers: [boardContainers], // The localized article is registered on the API side only, so its // generated CRUD *and* translation routes exist and its staff permissions diff --git a/plugins/example/src/config.api.ts b/plugins/example/src/config.api.ts index 1a68b8c30e..8f2a24caf6 100644 --- a/plugins/example/src/config.api.ts +++ b/plugins/example/src/config.api.ts @@ -1,12 +1,11 @@ import type { ApiPluginContract } from "@vitnode/core/api/lib/plugin"; import { buildApiPlugin } from "@vitnode/core/api/lib/plugin"; -import { buildContentPublicModule } from "@vitnode/core/content/server"; import { articleExcerptAiAction } from "@/api/ai/actions"; +import { buildContentPublicModule } from "@/api/builders"; import { exampleCategoryScope } from "@/api/lib/moderation"; import { adminModule } from "@/api/modules/admin/admin.module"; -import { CONFIG_PLUGIN } from "@/const"; import { boardContainers } from "@/content/boards"; import { settingsPage } from "@/content/settings-page"; import { advancedArticleContent } from "@/database/advanced-articles"; @@ -17,13 +16,14 @@ import { eventContent } from "@/database/events"; import { localizedArticleContent } from "@/database/localized-articles"; import { pageContent } from "@/database/pages"; import { tagContent } from "@/database/tags"; +import { manifest } from "@/manifest.gen"; import { widgets } from "./widgets"; import "@/api/lib/events"; export const exampleApiPlugin = () => buildApiPlugin({ - pluginId: CONFIG_PLUGIN.pluginId, + manifest, aiActions: [articleExcerptAiAction], blocks: widgets, editablePages: [settingsPage], @@ -37,7 +37,6 @@ export const exampleApiPlugin = () => modules: [ adminModule, buildContentPublicModule({ - pluginId: CONFIG_PLUGIN.pluginId, containers: [boardContainers], contentTypes: [ advancedArticleContent, diff --git a/plugins/example/src/config.tsx b/plugins/example/src/config.tsx index 0889252d1c..a0b6b2dc96 100644 --- a/plugins/example/src/config.tsx +++ b/plugins/example/src/config.tsx @@ -1,13 +1,12 @@ import { buildPlugin } from "@vitnode/core/lib/plugin"; -import { CONFIG_PLUGIN } from "@/const"; - import messages from "./locales"; +import { manifest } from "./manifest.gen"; import { routes } from "./routes"; export const examplePlugin = () => buildPlugin({ - ...CONFIG_PLUGIN, + manifest, localeFiles: { en: "@vitnode/example/locales/en.json", }, diff --git a/plugins/example/src/const.ts b/plugins/example/src/const.ts index c4af54fea3..5a9e7d7042 100644 --- a/plugins/example/src/const.ts +++ b/plugins/example/src/const.ts @@ -1,5 +1,3 @@ -export const CONFIG_PLUGIN = { pluginId: "@vitnode/example" as const }; - export const EXAMPLE_MIGRATIONS = [ "add_example_content", "add_publication_to_example_articles", diff --git a/plugins/example/src/content/settings-page.ts b/plugins/example/src/content/settings-page.ts index fdc692e544..a80e66011e 100644 --- a/plugins/example/src/content/settings-page.ts +++ b/plugins/example/src/content/settings-page.ts @@ -1,6 +1,6 @@ import { defineEditablePage } from "@vitnode/core/content/editor"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { PAGE_BLOCKS_ALLOWED, @@ -165,7 +165,7 @@ export const mayEditSettingsPage = ({ root || permissions.some( entry => - entry.plugin === CONFIG_PLUGIN.pluginId && + entry.plugin === manifest.id && entry.module === EXAMPLE_WIDGETS_PERMISSION.module && entry.permission === EXAMPLE_WIDGETS_PERMISSION.permission, ); diff --git a/plugins/example/src/manifest.gen.ts b/plugins/example/src/manifest.gen.ts new file mode 100644 index 0000000000..cff9eb31d5 --- /dev/null +++ b/plugins/example/src/manifest.gen.ts @@ -0,0 +1,24 @@ +/* oxlint-disable */ + +// This file is generated by VitNode. Do not edit it, and do not format it. +// +// It is rewritten by `vitnode build` and `vitnode dev` from this package's +// package.json - `name`, `version`, `description`, `author` and `license` - and the title and +// icon in `src/plugin-info.ts`. Pass `manifest` to `buildPlugin`, +// `buildApiPlugin` and `bindApiPlugin` - it is where the plugin id lives. + +import { pluginInfo } from "./plugin-info" + +export const manifest = { + id: "@vitnode/example", + title: pluginInfo.title ?? "@vitnode/example", + icon: pluginInfo.icon ?? null, + description: "Reference plugin exercising the VitNode Content Engine end to end.", + author: { + name: "VitNode", + email: "contact@vitnode.com", + url: "https://vitnode.com", + }, + license: "MIT", + version: "2.0.0-canary.13", +} as const diff --git a/plugins/example/src/pages/advanced-article-page.tsx b/plugins/example/src/pages/advanced-article-page.tsx index 21b942d5d0..376952f7fe 100644 --- a/plugins/example/src/pages/advanced-article-page.tsx +++ b/plugins/example/src/pages/advanced-article-page.tsx @@ -10,7 +10,7 @@ import { fetcher } from "@vitnode/core/tanstack/fetcher"; import { useTranslations } from "use-intl"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { ContentArticle } from "./content-article"; @@ -36,7 +36,7 @@ const loadAdvancedArticle = async ({ }) => { const [resolution, detail] = await Promise.all([ fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/advanced-articles", path: "/delivery/resolve/{slug}", @@ -46,7 +46,7 @@ const loadAdvancedArticle = async ({ }, }), fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/advanced-articles", path: "/{slug}", diff --git a/plugins/example/src/pages/article-page.tsx b/plugins/example/src/pages/article-page.tsx index b8ba98bb62..cf7c21eb76 100644 --- a/plugins/example/src/pages/article-page.tsx +++ b/plugins/example/src/pages/article-page.tsx @@ -9,7 +9,7 @@ import { import { fetcher } from "@vitnode/core/tanstack/fetcher"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { ContentArticle } from "./content-article"; @@ -22,14 +22,14 @@ const zodArticle = z.object({ const loadArticle = async (slug: string) => { const [resolution, detail] = await Promise.all([ fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/articles", path: "/delivery/resolve/{slug}", args: { params: { slug: encodeURIComponent(slug) } }, }), fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/articles", path: "/{slug}", diff --git a/plugins/example/src/pages/board-page.tsx b/plugins/example/src/pages/board-page.tsx index 840008d639..e6ca0577fd 100644 --- a/plugins/example/src/pages/board-page.tsx +++ b/plugins/example/src/pages/board-page.tsx @@ -26,7 +26,7 @@ import { Fragment } from "react"; import { useTranslations } from "use-intl"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import type { BoardSearch } from "./board-search"; @@ -45,7 +45,7 @@ const zodDiscussionRow = z.object({ const loadBoard = async (idOrSlug: string, { cursor }: BoardSearch) => { const detail = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/boards", path: "/{idOrSlug}", @@ -66,7 +66,7 @@ const loadBoard = async (idOrSlug: string, { cursor }: BoardSearch) => { }; const list = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/discussions", path: "/", diff --git a/plugins/example/src/pages/boards-data.ts b/plugins/example/src/pages/boards-data.ts index 40b668e05a..003f0ac3cb 100644 --- a/plugins/example/src/pages/boards-data.ts +++ b/plugins/example/src/pages/boards-data.ts @@ -1,10 +1,10 @@ import { fetcher } from "@vitnode/core/tanstack/fetcher"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; export const fetchBoards = async () => { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/boards", path: "/", diff --git a/plugins/example/src/pages/discussion-edit-page.tsx b/plugins/example/src/pages/discussion-edit-page.tsx index 4eb9c374b9..20fbf661eb 100644 --- a/plugins/example/src/pages/discussion-edit-page.tsx +++ b/plugins/example/src/pages/discussion-edit-page.tsx @@ -27,7 +27,7 @@ import { useRef, useState } from "react"; import { toast } from "sonner"; import { useTranslations } from "use-intl"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { DiscussionForm, @@ -44,7 +44,7 @@ const loadEditableDiscussion = async (param: string) => { } const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/discussions", path: "/{id}/edit", @@ -97,7 +97,7 @@ const DiscussionEditForm = ({ record }: { record: EditableDiscussion }) => { form, ) => { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "patch", module: "content/discussions", path: "/{id}", diff --git a/plugins/example/src/pages/discussion-new-page.tsx b/plugins/example/src/pages/discussion-new-page.tsx index 91e9aa7308..f6417e817b 100644 --- a/plugins/example/src/pages/discussion-new-page.tsx +++ b/plugins/example/src/pages/discussion-new-page.tsx @@ -21,7 +21,7 @@ import { FolderTreeIcon } from "lucide-react"; import { useState } from "react"; import { useTranslations } from "use-intl"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import type { DiscussionNewSearch } from "./discussion-search"; @@ -75,7 +75,7 @@ const DiscussionNewPage = ({ form, ) => { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "content/discussions", path: "/", diff --git a/plugins/example/src/pages/discussion-page.tsx b/plugins/example/src/pages/discussion-page.tsx index da971970ad..60bcb7826a 100644 --- a/plugins/example/src/pages/discussion-page.tsx +++ b/plugins/example/src/pages/discussion-page.tsx @@ -14,7 +14,7 @@ import { toast } from "sonner"; import { useTranslations } from "use-intl"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import type { DiscussionSearch } from "./discussion-search"; @@ -32,7 +32,7 @@ const zodDiscussion = z.object({ const loadDiscussion = async (slug: string) => { const detail = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/discussions", path: "/{slug}", @@ -44,7 +44,7 @@ const loadDiscussion = async (slug: string) => { ); const board = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/boards", path: "/{idOrSlug}", diff --git a/plugins/example/src/pages/event-page.tsx b/plugins/example/src/pages/event-page.tsx index 3aefdb0f7c..5016bd9dbd 100644 --- a/plugins/example/src/pages/event-page.tsx +++ b/plugins/example/src/pages/event-page.tsx @@ -9,7 +9,7 @@ import { fetcher } from "@vitnode/core/tanstack/fetcher"; import { useTranslations } from "use-intl"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { ContentArticle } from "./content-article"; @@ -31,7 +31,7 @@ const loadEvent = async ({ slug: string; }) => { const detail = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/events", path: "/{slug}", diff --git a/plugins/example/src/pages/localized-article-page.tsx b/plugins/example/src/pages/localized-article-page.tsx index 024e3ecceb..b6dad89ef6 100644 --- a/plugins/example/src/pages/localized-article-page.tsx +++ b/plugins/example/src/pages/localized-article-page.tsx @@ -6,7 +6,7 @@ import { contentPageItem } from "@vitnode/core/tanstack/content"; import { fetcher } from "@vitnode/core/tanstack/fetcher"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { ContentArticle } from "./content-article"; @@ -24,7 +24,7 @@ const loadLocalizedArticle = async ({ slug: string; }) => { const detail = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/localized-articles", path: "/{slug}", diff --git a/plugins/example/src/plugin-info.ts b/plugins/example/src/plugin-info.ts new file mode 100644 index 0000000000..526718e03c --- /dev/null +++ b/plugins/example/src/plugin-info.ts @@ -0,0 +1,6 @@ +import type { PluginInfo } from "@vitnode/core/lib/plugin-manifest"; + +export const pluginInfo: PluginInfo = { + title: "Example", + icon: "flask-conical", +}; diff --git a/plugins/example/src/plugin-metadata.gen.ts b/plugins/example/src/plugin-metadata.gen.ts new file mode 100644 index 0000000000..f79bff5bd3 --- /dev/null +++ b/plugins/example/src/plugin-metadata.gen.ts @@ -0,0 +1,16 @@ +/* oxlint-disable */ + +// This file is generated by VitNode. Do not edit it, and do not format it. +// +// It is rewritten by `vitnode build` and `vitnode dev` from this package's +// package.json - `name`, `version` and `description` - and the title and +// icon in `src/plugin-metadata.ts`. Register `pluginMetadata` as `metadata` +// in both `buildPlugin` and `buildApiPlugin`. + +export const pluginMetadata = { + id: "@vitnode/example", + title: "@vitnode/example", + icon: null, + description: "Reference plugin exercising the VitNode Content Engine end to end.", + version: "2.0.0-canary.13", +} as const diff --git a/plugins/example/src/widgets.tsx b/plugins/example/src/widgets.tsx index 56893dcffd..28a5721689 100644 --- a/plugins/example/src/widgets.tsx +++ b/plugins/example/src/widgets.tsx @@ -1,6 +1,6 @@ import type { WidgetPluginSource } from "@vitnode/core/widgets"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { calloutWidget } from "./widgets/callout"; import { featuresWidget } from "./widgets/features"; @@ -15,7 +15,7 @@ export { } from "./widgets/features"; export const widgets = { - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, blocks: [calloutWidget, featuresWidget], } satisfies WidgetPluginSource; diff --git a/plugins/forum/package.json b/plugins/forum/package.json index 81668dc526..686d0726ad 100644 --- a/plugins/forum/package.json +++ b/plugins/forum/package.json @@ -2,6 +2,11 @@ "name": "@vitnode/forum", "version": "2.0.0-canary.13", "description": "Community forum for VitNode: structured forums and nested threads, built on the Content Engine.", + "author": { + "name": "VitNode", + "email": "contact@vitnode.com", + "url": "https://vitnode.com" + }, "license": "MIT", "private": true, "type": "module", diff --git a/plugins/forum/src/admin/content.tsx b/plugins/forum/src/admin/content.tsx index 2a6b0ed42d..771faa56ca 100644 --- a/plugins/forum/src/admin/content.tsx +++ b/plugins/forum/src/admin/content.tsx @@ -1,10 +1,10 @@ import type { ContentFrontendPluginSource } from "@vitnode/core/lib/plugin"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { forumForumsNav } from "./nav"; export const adminContent = { - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, containers: [forumForumsNav], } satisfies ContentFrontendPluginSource; diff --git a/plugins/forum/src/admin/nav.tsx b/plugins/forum/src/admin/nav.tsx index 04b2090f74..ee0d774c4b 100644 --- a/plugins/forum/src/admin/nav.tsx +++ b/plugins/forum/src/admin/nav.tsx @@ -2,8 +2,8 @@ import type { AdminNavPluginSource } from "@vitnode/core/lib/plugin"; import { FolderTreeIcon } from "lucide-react"; -import { CONFIG_PLUGIN } from "@/const"; import { forumContainers } from "@/content/forums"; +import { manifest } from "@/manifest.gen"; export const forumForumsNav = { definition: forumContainers, @@ -11,6 +11,6 @@ export const forumForumsNav = { }; export const adminNav = { - pluginId: CONFIG_PLUGIN.pluginId, + pluginId: manifest.id, containers: [forumForumsNav], } satisfies AdminNavPluginSource; diff --git a/plugins/forum/src/api/builders.ts b/plugins/forum/src/api/builders.ts new file mode 100644 index 0000000000..1ab9de62d3 --- /dev/null +++ b/plugins/forum/src/api/builders.ts @@ -0,0 +1,8 @@ +import { bindApiPlugin } from "@vitnode/core/api/lib/bind-api-plugin"; +import { bindContentPlugin } from "@vitnode/core/content/server"; + +import { manifest } from "@/manifest.gen"; + +export const { buildModule, buildRoute } = bindApiPlugin(manifest); +export const { buildContentAdminModule, buildContentPublicModule } = + bindContentPlugin(manifest); diff --git a/plugins/forum/src/api/modules/admin/admin.module.ts b/plugins/forum/src/api/modules/admin/admin.module.ts index 60aad839f4..288bb7e867 100644 --- a/plugins/forum/src/api/modules/admin/admin.module.ts +++ b/plugins/forum/src/api/modules/admin/admin.module.ts @@ -1,18 +1,13 @@ -import { buildModule } from "@vitnode/core/api/lib/module"; -import { buildContentAdminModule } from "@vitnode/core/content/server"; - -import { CONFIG_PLUGIN } from "@/const"; +import { buildContentAdminModule, buildModule } from "@/api/builders"; import { forumContainers } from "@/content/forums"; import { commentContent } from "@/database/comments"; import { topicContent } from "@/database/topics"; export const adminModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "admin", routes: [], modules: [ buildContentAdminModule({ - pluginId: CONFIG_PLUGIN.pluginId, containers: [forumContainers], contentTypes: [topicContent, commentContent], }), diff --git a/plugins/forum/src/api/modules/forum/forum.module.ts b/plugins/forum/src/api/modules/forum/forum.module.ts index 2f7f2a0c9d..c84c5ac999 100644 --- a/plugins/forum/src/api/modules/forum/forum.module.ts +++ b/plugins/forum/src/api/modules/forum/forum.module.ts @@ -1,6 +1,4 @@ -import { buildModule } from "@vitnode/core/api/lib/module"; - -import { CONFIG_PLUGIN } from "@/const"; +import { buildModule } from "@/api/builders"; import { forumStatsRoute, @@ -9,7 +7,6 @@ import { } from "./stats.routes"; export const forumModule = buildModule({ - pluginId: CONFIG_PLUGIN.pluginId, name: "forum", routes: [forumStatsRoute, topicStatsRoute, topicSlugRoute], }); diff --git a/plugins/forum/src/api/modules/forum/stats.routes.ts b/plugins/forum/src/api/modules/forum/stats.routes.ts index d68d2ecf84..1c724c78dc 100644 --- a/plugins/forum/src/api/modules/forum/stats.routes.ts +++ b/plugins/forum/src/api/modules/forum/stats.routes.ts @@ -2,7 +2,6 @@ import type { PgColumn } from "drizzle-orm/pg-core"; import { z } from "@hono/zod-openapi"; import { setPageCacheTags } from "@vitnode/core/api/lib/page-cache"; -import { buildRoute } from "@vitnode/core/api/lib/route"; import { contentContainerTag, contentPublicListTag, @@ -14,7 +13,7 @@ import { import { and, count, desc, eq, inArray, max } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; -import { CONFIG_PLUGIN } from "@/const"; +import { buildRoute } from "@/api/builders"; import { forumContainers } from "@/content/forums"; import { commentContent } from "@/database/comments"; import { topicContent } from "@/database/topics"; @@ -54,7 +53,6 @@ const zodLatestTopic = z.object({ }); export const forumStatsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", path: "/stats/forums", @@ -143,7 +141,6 @@ const TOPIC_IDS = new RegExp( ); export const topicStatsRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", path: "/stats/topics", @@ -209,7 +206,6 @@ export const topicStatsRoute = buildRoute({ }); export const topicSlugRoute = buildRoute({ - pluginId: CONFIG_PLUGIN.pluginId, route: { method: "get", path: "/topics/{id}/slug", diff --git a/plugins/forum/src/config.api.ts b/plugins/forum/src/config.api.ts index f0fc8ae60d..dec30e9e53 100644 --- a/plugins/forum/src/config.api.ts +++ b/plugins/forum/src/config.api.ts @@ -1,24 +1,23 @@ import type { ApiPluginContract } from "@vitnode/core/api/lib/plugin"; import { buildApiPlugin } from "@vitnode/core/api/lib/plugin"; -import { buildContentPublicModule } from "@vitnode/core/content/server"; +import { buildContentPublicModule } from "@/api/builders"; import { adminModule } from "@/api/modules/admin/admin.module"; import { forumModule } from "@/api/modules/forum/forum.module"; -import { CONFIG_PLUGIN } from "@/const"; import { forumContainers } from "@/content/forums"; import { commentContent } from "@/database/comments"; import { topicContent } from "@/database/topics"; +import { manifest } from "@/manifest.gen"; export const forumApiPlugin = () => buildApiPlugin({ - pluginId: CONFIG_PLUGIN.pluginId, + manifest, navigation: [{ href: "/forum", icon: "messages-square", id: "forum" }], modules: [ adminModule, forumModule, buildContentPublicModule({ - pluginId: CONFIG_PLUGIN.pluginId, containers: [forumContainers], contentTypes: [topicContent, commentContent], }), diff --git a/plugins/forum/src/config.tsx b/plugins/forum/src/config.tsx index 071db6fa70..ea8fb38ff0 100644 --- a/plugins/forum/src/config.tsx +++ b/plugins/forum/src/config.tsx @@ -1,13 +1,12 @@ import { buildPlugin } from "@vitnode/core/lib/plugin"; -import { CONFIG_PLUGIN } from "@/const"; - import messages from "./locales"; +import { manifest } from "./manifest.gen"; import { routes } from "./routes"; export const forumPlugin = () => buildPlugin({ - ...CONFIG_PLUGIN, + manifest, localeFiles: { en: "@vitnode/forum/locales/en.json", }, diff --git a/plugins/forum/src/const.ts b/plugins/forum/src/const.ts deleted file mode 100644 index 016b8ead08..0000000000 --- a/plugins/forum/src/const.ts +++ /dev/null @@ -1 +0,0 @@ -export const CONFIG_PLUGIN = { pluginId: "@vitnode/forum" as const }; diff --git a/plugins/forum/src/manifest.gen.ts b/plugins/forum/src/manifest.gen.ts new file mode 100644 index 0000000000..7c648daf61 --- /dev/null +++ b/plugins/forum/src/manifest.gen.ts @@ -0,0 +1,24 @@ +/* oxlint-disable */ + +// This file is generated by VitNode. Do not edit it, and do not format it. +// +// It is rewritten by `vitnode build` and `vitnode dev` from this package's +// package.json - `name`, `version`, `description`, `author` and `license` - and the title and +// icon in `src/plugin-info.ts`. Pass `manifest` to `buildPlugin`, +// `buildApiPlugin` and `bindApiPlugin` - it is where the plugin id lives. + +import { pluginInfo } from "./plugin-info" + +export const manifest = { + id: "@vitnode/forum", + title: pluginInfo.title ?? "@vitnode/forum", + icon: pluginInfo.icon ?? null, + description: "Community forum for VitNode: structured forums and nested threads, built on the Content Engine.", + author: { + name: "VitNode", + email: "contact@vitnode.com", + url: "https://vitnode.com", + }, + license: "MIT", + version: "2.0.0-canary.13", +} as const diff --git a/plugins/forum/src/pages/comment-composer.tsx b/plugins/forum/src/pages/comment-composer.tsx index 96ad12752b..78f6811010 100644 --- a/plugins/forum/src/pages/comment-composer.tsx +++ b/plugins/forum/src/pages/comment-composer.tsx @@ -16,7 +16,7 @@ import { toast } from "sonner"; import { useTranslations } from "use-intl"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { useForumRejection } from "./forum-errors"; @@ -70,7 +70,7 @@ const CommentComposer = (props: CommentComposerProps) => { const onSubmit: AutoFormOnSubmit = async ({ body }, form) => { if (props.mode === "edit") { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "patch", module: "content/comments", path: "/{id}", @@ -99,7 +99,7 @@ const CommentComposer = (props: CommentComposerProps) => { } const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "content/comments", path: "/", diff --git a/plugins/forum/src/pages/comment-redirect-page.tsx b/plugins/forum/src/pages/comment-redirect-page.tsx index 01b6045c2c..aad72e52af 100644 --- a/plugins/forum/src/pages/comment-redirect-page.tsx +++ b/plugins/forum/src/pages/comment-redirect-page.tsx @@ -8,7 +8,7 @@ import { fetcher } from "@vitnode/core/tanstack/fetcher"; import { defineRoute } from "@vitnode/core/tanstack/plugin-routes"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { commentAnchorId, topicHref } from "./forum-links"; import { @@ -25,7 +25,7 @@ const commentLinkQueryOptions = (slug: string) => meta: LOADER_DATA_QUERY_META, queryFn: async () => { const comment = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/comments", path: "/{slug}", @@ -40,7 +40,7 @@ const commentLinkQueryOptions = (slug: string) => const record = zodCommentLink.parse(await comment.json()); const topic = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "forum", path: "/topics/{id}/slug", diff --git a/plugins/forum/src/pages/forum-data.ts b/plugins/forum/src/pages/forum-data.ts index b6a9d61021..a6892013bb 100644 --- a/plugins/forum/src/pages/forum-data.ts +++ b/plugins/forum/src/pages/forum-data.ts @@ -2,11 +2,11 @@ import { zodContentPublicPerson } from "@vitnode/core/content"; import { fetcher } from "@vitnode/core/tanstack/fetcher"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; export const fetchForums = async () => { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/forums", path: "/", @@ -24,7 +24,7 @@ export type Forum = Awaited>[number]; export const fetchForumStats = async (): Promise => { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "forum", path: "/stats/forums", @@ -56,7 +56,7 @@ export const fetchTopicStats = async ( if (unique.length === 0) return []; const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "forum", path: "/stats/topics", diff --git a/plugins/forum/src/pages/forum-page.tsx b/plugins/forum/src/pages/forum-page.tsx index 8fbd7688be..eac48275d0 100644 --- a/plugins/forum/src/pages/forum-page.tsx +++ b/plugins/forum/src/pages/forum-page.tsx @@ -32,7 +32,7 @@ import { Fragment } from "react"; import { useTranslations } from "use-intl"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import type { TopicRow, TopicStats } from "./forum-data"; import type { ForumSearch } from "./forum-links"; @@ -65,7 +65,7 @@ const forumTopicsQueryOptions = (request: ForumSearch & { forumId: number }) => const filters = { forum: String(request.forumId) }; const list = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/topics", path: "/", diff --git a/plugins/forum/src/pages/forum-queries.ts b/plugins/forum/src/pages/forum-queries.ts index c94d79e08c..7e4a90ca27 100644 --- a/plugins/forum/src/pages/forum-queries.ts +++ b/plugins/forum/src/pages/forum-queries.ts @@ -8,7 +8,7 @@ import { } from "@vitnode/core/lib/query-freshness"; import { fetcher } from "@vitnode/core/tanstack/fetcher"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import type { ForumSearch } from "./forum-links"; @@ -23,7 +23,7 @@ export const FORUM_ACTIVITY_STALE_TIME = OPERATIONAL_STALE_TIME; export const PENDING_STALE_TIME = OPERATIONAL_STALE_TIME; -const all = [CONFIG_PLUGIN.pluginId] as const; +const all = [manifest.id] as const; const commentLinks = [...all, "comment-link"] as const; const submissions = [...all, "pending"] as const; const topicDetails = [...all, "topic"] as const; @@ -71,7 +71,7 @@ export const forumQueryOptions = (idOrSlug: string) => meta: LOADER_DATA_QUERY_META, queryFn: async () => { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/forums", path: "/{idOrSlug}", diff --git a/plugins/forum/src/pages/pending-page.tsx b/plugins/forum/src/pages/pending-page.tsx index 5a51bd5f98..a169594d0a 100644 --- a/plugins/forum/src/pages/pending-page.tsx +++ b/plugins/forum/src/pages/pending-page.tsx @@ -22,9 +22,9 @@ import { InboxIcon } from "lucide-react"; import { useTranslations } from "use-intl"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; import { commentContentType } from "@/content/comment"; import { topicContentType } from "@/content/topic"; +import { manifest } from "@/manifest.gen"; import type { PendingSearch } from "./forum-links"; @@ -84,7 +84,7 @@ const pendingQueryOptions = ({ items: z .array(zodSubmission) .parse(data.items) - .filter(item => item.pluginId === CONFIG_PLUGIN.pluginId), + .filter(item => item.pluginId === manifest.id), page: data.page, pageCount: Math.max(1, Math.ceil(data.total / data.pageSize)), }; diff --git a/plugins/forum/src/pages/thread-data.ts b/plugins/forum/src/pages/thread-data.ts index 9d1ca2bf97..f563e60274 100644 --- a/plugins/forum/src/pages/thread-data.ts +++ b/plugins/forum/src/pages/thread-data.ts @@ -2,7 +2,7 @@ import { zodRichTextDocument } from "@vitnode/core/content"; import { fetcher } from "@vitnode/core/tanstack/fetcher"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import { zodPerson } from "./forum-data"; @@ -68,7 +68,7 @@ export const fetchThreadPage = async ({ topicId: number; }): Promise => { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/comments", path: "/thread", diff --git a/plugins/forum/src/pages/thread.tsx b/plugins/forum/src/pages/thread.tsx index 85ad8bf9d3..3e90963e4d 100644 --- a/plugins/forum/src/pages/thread.tsx +++ b/plugins/forum/src/pages/thread.tsx @@ -25,8 +25,8 @@ import { toast } from "sonner"; import { useTranslations } from "use-intl"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; import { FORUM_COMMENT_MAX_DEPTH } from "@/content/comment"; +import { manifest } from "@/manifest.gen"; import type { CommentComposerProps } from "./comment-composer"; import type { ThreadComment, ThreadPage, ThreadTreeNode } from "./thread-data"; @@ -69,7 +69,7 @@ const useCommentEditor = () => { return async (commentId: number) => { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/comments", path: "/{id}/edit", diff --git a/plugins/forum/src/pages/topic-edit-page.tsx b/plugins/forum/src/pages/topic-edit-page.tsx index f8b8ecd884..1c80b0372f 100644 --- a/plugins/forum/src/pages/topic-edit-page.tsx +++ b/plugins/forum/src/pages/topic-edit-page.tsx @@ -35,7 +35,7 @@ import { toast } from "sonner"; import { useTranslations } from "use-intl"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import type { TopicEditSearch } from "./forum-links"; import type { TopicFormValues } from "./topic-form"; @@ -70,7 +70,7 @@ const loadEditableTopic = async (param: string) => { } const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/topics", path: "/{id}/edit", @@ -209,7 +209,7 @@ const TopicEditForm = ({ const onSubmit: AutoFormOnSubmit = async (values, form) => { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "patch", module: "content/topics", path: "/{id}", diff --git a/plugins/forum/src/pages/topic-new-page.tsx b/plugins/forum/src/pages/topic-new-page.tsx index 15c695d9e8..73ae1e7815 100644 --- a/plugins/forum/src/pages/topic-new-page.tsx +++ b/plugins/forum/src/pages/topic-new-page.tsx @@ -22,7 +22,7 @@ import { FolderTreeIcon } from "lucide-react"; import { useState } from "react"; import { useTranslations } from "use-intl"; -import { CONFIG_PLUGIN } from "@/const"; +import { manifest } from "@/manifest.gen"; import type { TopicNewSearch } from "./forum-links"; @@ -78,7 +78,7 @@ const TopicNewPage = ({ form, ) => { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "post", module: "content/topics", path: "/", diff --git a/plugins/forum/src/pages/topic-page.tsx b/plugins/forum/src/pages/topic-page.tsx index 03fc96723f..6c051a6bb9 100644 --- a/plugins/forum/src/pages/topic-page.tsx +++ b/plugins/forum/src/pages/topic-page.tsx @@ -28,8 +28,8 @@ import { toast } from "sonner"; import { useTranslations } from "use-intl"; import { z } from "zod"; -import { CONFIG_PLUGIN } from "@/const"; import { topicContentType } from "@/content/topic"; +import { manifest } from "@/manifest.gen"; import type { ForumPerson } from "./forum-data"; import type { TopicSearch } from "./forum-links"; @@ -87,7 +87,7 @@ const topicQueryOptions = (slug: string) => meta: LOADER_DATA_QUERY_META, queryFn: async () => { const res = await fetcher({ - plugin: CONFIG_PLUGIN.pluginId, + plugin: manifest.id, method: "get", module: "content/topics", path: "/{slug}", diff --git a/plugins/forum/src/plugin-info.ts b/plugins/forum/src/plugin-info.ts new file mode 100644 index 0000000000..024a4c827c --- /dev/null +++ b/plugins/forum/src/plugin-info.ts @@ -0,0 +1,6 @@ +import type { PluginInfo } from "@vitnode/core/lib/plugin-manifest"; + +export const pluginInfo: PluginInfo = { + title: "Forum", + icon: "messages-square", +}; diff --git a/plugins/forum/src/plugin-metadata.gen.ts b/plugins/forum/src/plugin-metadata.gen.ts new file mode 100644 index 0000000000..aa00426548 --- /dev/null +++ b/plugins/forum/src/plugin-metadata.gen.ts @@ -0,0 +1,16 @@ +/* oxlint-disable */ + +// This file is generated by VitNode. Do not edit it, and do not format it. +// +// It is rewritten by `vitnode build` and `vitnode dev` from this package's +// package.json - `name`, `version` and `description` - and the title and +// icon in `src/plugin-metadata.ts`. Register `pluginMetadata` as `metadata` +// in both `buildPlugin` and `buildApiPlugin`. + +export const pluginMetadata = { + id: "@vitnode/forum", + title: "@vitnode/forum", + icon: null, + description: "Community forum for VitNode: structured forums and nested threads, built on the Content Engine.", + version: "2.0.0-canary.13", +} as const diff --git a/scripts/bump-version/version-manager.ts b/scripts/bump-version/version-manager.ts index 40594097c8..95a7984db2 100644 --- a/scripts/bump-version/version-manager.ts +++ b/scripts/bump-version/version-manager.ts @@ -109,7 +109,7 @@ export class VersionManager { "packages", "vitnode", "src", - "config.ts", + "manifest.ts", ); const cleanVersion = version.replace(/^v/, ""); const content = readFileSync(configPath, "utf8");