@wervt/nuxt 0.2.8 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,6 +19,7 @@ export default defineNuxtConfig({
19
19
  - `proxyShape()` (server) and `useShape()` (app): live rows from Postgres via Electric.
20
20
  - `useWervtUser()`: the signed-in wervt user, on the server and in the app.
21
21
  - `useValidatedBody()` / `useValidatedQuery()` / `useValidatedParams()`: valibot validation.
22
+ - `defineTool()` (server): tools in `server/tools/` that browser agents call through WebMCP.
22
23
  - `nuxt dev` starts against the local wervt stack (Postgres, Electric) and a dev user.
23
24
 
24
25
  Deploy with [`@wervt/cli`](https://www.npmjs.com/package/@wervt/cli): `wervt deploy`.
package/dist/module.d.ts CHANGED
@@ -7,6 +7,17 @@ export interface ModuleOptions {
7
7
  dev?: DevOptions;
8
8
  /** Time zone of `nitro.scheduledTasks`, e.g. `Europe/Berlin`. Defaults to UTC. */
9
9
  timezone?: string;
10
+ /**
11
+ * When a new release is live while the app is open: `'banner'` (default) shows a small
12
+ * "A new version is available · Reload" banner, `'manual'` only sets `useWervtUpdate()` for
13
+ * the app's own banner, `false` leaves it to Nuxt (reload on the next navigation, hourly).
14
+ */
15
+ updates?: 'banner' | 'manual' | false;
16
+ /**
17
+ * Registers the tools in `server/tools/` (`defineTool`) with browser agents through WebMCP.
18
+ * Defaults to true; `false` leaves them out of the build.
19
+ */
20
+ webmcp?: boolean;
10
21
  }
11
22
  /** `server/wervt-tasks.json` in the build: the app's tasks and schedules, for the control plane. */
12
23
  export interface TasksManifest {
package/dist/module.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { resolveAppName, setupDev } from "./dev.js";
2
- import { addImports, addPlugin, addServerHandler, addServerPlugin, createResolver, defineNuxtModule, getNuxtVersion, hasNuxtModule, installModule } from "nuxt/kit";
3
- import { mkdir, writeFile } from "node:fs/promises";
2
+ import { addImports, addPlugin, addServerHandler, addServerPlugin, addServerTemplate, createResolver, defineNuxtModule, getNuxtVersion, hasNuxtModule, installModule } from "nuxt/kit";
3
+ import { mkdir, readdir, writeFile } from "node:fs/promises";
4
4
  import { builtinModules, createRequire } from "node:module";
5
- import { dirname, join } from "node:path";
5
+ import { dirname, extname, join, relative, resolve, sep } from "node:path";
6
6
  import evlog from "evlog/nuxt";
7
7
  //#region src/module.ts
8
8
  const CONTENT_DATABASE = {
@@ -61,7 +61,7 @@ var module_default = defineNuxtModule({
61
61
  inline: [
62
62
  ...options.externals?.inline ?? [],
63
63
  resolver.resolve("./runtime"),
64
- /(^|\/node_modules\/)(drizzle-orm|postgres|valibot|@electric-sql\/client|evlog)(\/|$)/
64
+ /(^|\/node_modules\/)(drizzle-orm|postgres|valibot|@valibot\/to-json-schema|@electric-sql\/client|evlog)(\/|$)/
65
65
  ]
66
66
  };
67
67
  const exclude = options.esbuild?.options?.exclude ?? /node_modules/;
@@ -161,6 +161,32 @@ var module_default = defineNuxtModule({
161
161
  });
162
162
  });
163
163
  }
164
+ const toolsDir = join(nuxt.options.serverDir, "tools");
165
+ const tools = options.webmcp === false ? [] : await findTools(toolsDir);
166
+ if (tools.length > 0) {
167
+ addServerTemplate({
168
+ filename: "#wervt/tools",
169
+ getContents: () => `export const tools = {\n${tools.map(({ name, path }) => ` ${JSON.stringify(name)}: () => import(${JSON.stringify(path)}),`).join("\n")}\n}\n`
170
+ });
171
+ addServerHandler({
172
+ route: "/_wervt/tools",
173
+ method: "get",
174
+ handler: resolver.resolve("./runtime/server/tool-list")
175
+ });
176
+ addServerHandler({
177
+ route: "/_wervt/tools/**",
178
+ method: "post",
179
+ handler: resolver.resolve("./runtime/server/tool-run")
180
+ });
181
+ addPlugin({
182
+ src: resolver.resolve("./runtime/app/plugins/webmcp.client"),
183
+ mode: "client"
184
+ });
185
+ }
186
+ if (nuxt.options.dev && options.webmcp !== false) nuxt.hook("builder:watch", async (event, path) => {
187
+ const file = relative(toolsDir, resolve(nuxt.options.srcDir, path));
188
+ if ((event === "add" || event === "unlink") && !file.startsWith("..")) await nuxt.callHook("restart");
189
+ });
164
190
  if (nuxt.options.dev && !nuxt.options._prepare) {
165
191
  const app = await resolveAppName(nuxt.options.rootDir, options.app);
166
192
  await setupDev(nuxt, app, options.dev ?? {}, { storage: useAblage });
@@ -178,6 +204,19 @@ var module_default = defineNuxtModule({
178
204
  src: resolver.resolve("./runtime/app/plugins/user.client"),
179
205
  mode: "client"
180
206
  });
207
+ const updates = options.updates ?? "banner";
208
+ if (updates && !nuxt.options.dev) {
209
+ nuxt.options.experimental.checkOutdatedBuildInterval = false;
210
+ const config = nuxt.options.runtimeConfig.public;
211
+ config.wervt = {
212
+ ...config.wervt,
213
+ updates
214
+ };
215
+ addPlugin({
216
+ src: resolver.resolve("./runtime/app/plugins/update.client"),
217
+ mode: "client"
218
+ });
219
+ }
181
220
  nuxt.options.build.transpile.push(resolver.resolve("./runtime"));
182
221
  addImports([
183
222
  {
@@ -195,10 +234,40 @@ var module_default = defineNuxtModule({
195
234
  {
196
235
  name: "wervtLogoutUrl",
197
236
  from: resolver.resolve("./runtime/app/useWervtUser")
237
+ },
238
+ {
239
+ name: "useWervtUpdate",
240
+ from: resolver.resolve("./runtime/app/useWervtUpdate")
198
241
  }
199
242
  ]);
200
243
  }
201
244
  });
245
+ const TOOL_EXTENSIONS = /* @__PURE__ */ new Set([
246
+ ".ts",
247
+ ".js",
248
+ ".mts",
249
+ ".mjs"
250
+ ]);
251
+ const TOOL_NAME_RE = /^[\w.-]{1,128}$/;
252
+ /** The tools in `server/tools/`: `todos/add.ts` is `todos.add`, like Nitro names tasks. */
253
+ async function findTools(dir) {
254
+ const files = await readdir(dir, { recursive: true }).catch(() => []);
255
+ const tools = [];
256
+ for (const file of files.toSorted((a, b) => a.localeCompare(b))) {
257
+ const ext = extname(file);
258
+ if (!TOOL_EXTENSIONS.has(ext) || file.endsWith(`.d${ext}`)) continue;
259
+ const name = file.slice(0, -ext.length).split(sep).join(".");
260
+ if (!TOOL_NAME_RE.test(name)) {
261
+ console.warn(`[wervt] Skipping server/tools/${file}: tool names can only use A-Z, 0-9, _ and -.`);
262
+ continue;
263
+ }
264
+ tools.push({
265
+ name,
266
+ path: join(dir, file)
267
+ });
268
+ }
269
+ return tools;
270
+ }
202
271
  /** evlog's Nitro 2 plugin and error handler, as evlog/nuxt resolves them. */
203
272
  const EVLOG_NITRO2_RE = /([\\/]evlog[\\/]dist[\\/]nitro)[\\/](plugin|errorHandler)(\.mjs)?$/;
204
273
  const BUILTINS = new Set(builtinModules.filter((name) => !name.startsWith("_")));
@@ -0,0 +1,4 @@
1
+ //#region src/runtime/app/plugins/update.client.d.ts
2
+ declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
3
+ //#endregion
4
+ export { _default as default };
@@ -0,0 +1,78 @@
1
+ import { useUpdateAvailable, useWervtUpdate } from "../useWervtUpdate.js";
2
+ import { defineNuxtPlugin, getAppManifest, onNuxtReady, useRuntimeConfig } from "#app";
3
+ //#region src/runtime/app/plugins/update.client.ts
4
+ const INTERVAL_MS = 6e4;
5
+ /** Focus and visibility changes come in bursts; one check covers them. */
6
+ const MIN_GAP_MS = 1e4;
7
+ var update_client_default = defineNuxtPlugin({
8
+ name: "wervt:update",
9
+ setup(nuxtApp) {
10
+ const config = useRuntimeConfig();
11
+ const mode = config.public.wervt?.updates;
12
+ const available = useUpdateAvailable();
13
+ const url = `${(config.app.cdnURL || config.app.baseURL).replace(/\/$/, "")}/${config.app.buildAssetsDir.replace(/^\/|\/$/g, "")}/builds/latest.json`;
14
+ let last = 0;
15
+ async function check() {
16
+ if (available.value || document.hidden || Date.now() - last < MIN_GAP_MS) return;
17
+ last = Date.now();
18
+ try {
19
+ const current = await getAppManifest();
20
+ const res = await fetch(`${url}?${last}`, {
21
+ cache: "no-store",
22
+ signal: AbortSignal.timeout(1e4)
23
+ });
24
+ if (!res.ok) return;
25
+ const latest = await res.json();
26
+ if (!latest.id || latest.id === current.id) return;
27
+ available.value = true;
28
+ await nuxtApp.callHook("app:manifest:update", latest);
29
+ if (mode === "banner") showBanner(useWervtUpdate().reload);
30
+ } catch {}
31
+ }
32
+ onNuxtReady(() => {
33
+ setInterval(check, INTERVAL_MS);
34
+ document.addEventListener("visibilitychange", check);
35
+ window.addEventListener("focus", check);
36
+ });
37
+ }
38
+ });
39
+ /** A small pill at the bottom, plain DOM so it works with any styling (or none). */
40
+ function showBanner(reload) {
41
+ if (document.getElementById("wervt-update")) return;
42
+ const bar = document.createElement("div");
43
+ bar.id = "wervt-update";
44
+ bar.setAttribute("role", "status");
45
+ bar.style.cssText = [
46
+ "position:fixed",
47
+ "left:50%",
48
+ "bottom:max(16px, env(safe-area-inset-bottom))",
49
+ "transform:translateX(-50%)",
50
+ "z-index:2147483647",
51
+ "display:flex",
52
+ "align-items:center",
53
+ "gap:8px",
54
+ "padding:6px 6px 6px 16px",
55
+ "border-radius:999px",
56
+ "background:#18181b",
57
+ "color:#fafafa",
58
+ "font:500 14px/1.4 system-ui, sans-serif",
59
+ "box-shadow:0 4px 16px rgb(0 0 0 / 0.25)",
60
+ "white-space:nowrap"
61
+ ].join(";");
62
+ const text = document.createElement("span");
63
+ text.textContent = "A new version is available";
64
+ const button = (label, style, onClick) => {
65
+ const el = document.createElement("button");
66
+ el.type = "button";
67
+ el.textContent = label;
68
+ el.style.cssText = `border:0;border-radius:999px;padding:6px 12px;font:inherit;cursor:pointer;${style}`;
69
+ el.addEventListener("click", onClick);
70
+ return el;
71
+ };
72
+ const close = button("×", "background:transparent;color:inherit;padding:6px 10px", () => bar.remove());
73
+ close.setAttribute("aria-label", "Dismiss");
74
+ bar.append(text, button("Reload", "background:#fafafa;color:#18181b", reload), close);
75
+ document.body.append(bar);
76
+ }
77
+ //#endregion
78
+ export { update_client_default as default };
@@ -0,0 +1,4 @@
1
+ //#region src/runtime/app/plugins/webmcp.client.d.ts
2
+ declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
3
+ //#endregion
4
+ export { _default as default };
@@ -0,0 +1,50 @@
1
+ import { defineNuxtPlugin, onNuxtReady, useRuntimeConfig } from "#app";
2
+ //#region src/runtime/app/plugins/webmcp.client.ts
3
+ function pageModelContext() {
4
+ const context = document.modelContext ?? navigator.modelContext;
5
+ return typeof context?.registerTool === "function" ? context : void 0;
6
+ }
7
+ var webmcp_client_default = defineNuxtPlugin({
8
+ name: "wervt:webmcp",
9
+ setup() {
10
+ const context = pageModelContext();
11
+ if (!context) return;
12
+ const base = useRuntimeConfig().app.baseURL.replace(/\/$/, "");
13
+ async function execute(name, input) {
14
+ const res = await fetch(`${base}/_wervt/tools/${encodeURIComponent(name)}`, {
15
+ method: "POST",
16
+ headers: { "content-type": "application/json" },
17
+ body: JSON.stringify(input ?? {})
18
+ });
19
+ const body = await res.json().catch(() => void 0);
20
+ if (res.ok) return body?.result ?? null;
21
+ const issues = body?.issues ?? body?.data;
22
+ const message = (typeof body?.error === "string" ? body.error : body?.message) ?? `${res.status} ${res.statusText}`;
23
+ throw new Error(issues ? `${message} ${JSON.stringify(issues)}` : message);
24
+ }
25
+ onNuxtReady(async () => {
26
+ let tools;
27
+ try {
28
+ const res = await fetch(`${base}/_wervt/tools`);
29
+ if (!res.ok) return;
30
+ tools = await res.json();
31
+ } catch {
32
+ return;
33
+ }
34
+ for (const tool of tools) try {
35
+ const result = context.registerTool({
36
+ name: tool.name,
37
+ description: tool.description,
38
+ inputSchema: tool.inputSchema,
39
+ ...tool.readOnly ? { annotations: { readOnlyHint: true } } : {},
40
+ execute: (input) => execute(tool.name, input)
41
+ });
42
+ if (result instanceof Promise) result.catch((e) => console.warn(`wervt: could not register tool ${tool.name}`, e));
43
+ } catch (e) {
44
+ console.warn(`wervt: could not register tool ${tool.name}`, e);
45
+ }
46
+ });
47
+ }
48
+ });
49
+ //#endregion
50
+ export { webmcp_client_default as default };
@@ -0,0 +1,13 @@
1
+ //#region src/runtime/app/useWervtUpdate.d.ts
2
+ /** State behind `useWervtUpdate()`, set by the update check (`plugins/update.client`). */
3
+ export declare const useUpdateAvailable: () => import("vue").Ref<boolean, boolean>;
4
+ /**
5
+ * Whether a newer release of this app is live, and a reload into it. @wervt/nuxt checks every
6
+ * minute and when the tab comes back to the foreground. With `wervt: { updates: 'manual' }`
7
+ * the app shows its own banner from this instead of the built-in one.
8
+ */
9
+ export declare function useWervtUpdate(): {
10
+ available: Readonly<import("vue").Ref<boolean, boolean>>;
11
+ reload: () => void;
12
+ };
13
+ //#endregion
@@ -0,0 +1,21 @@
1
+ import { readonly } from "vue";
2
+ import { reloadNuxtApp, useState } from "#app";
3
+ //#region src/runtime/app/useWervtUpdate.ts
4
+ /** State behind `useWervtUpdate()`, set by the update check (`plugins/update.client`). */
5
+ const useUpdateAvailable = () => useState("wervt-update", () => false);
6
+ /**
7
+ * Whether a newer release of this app is live, and a reload into it. @wervt/nuxt checks every
8
+ * minute and when the tab comes back to the foreground. With `wervt: { updates: 'manual' }`
9
+ * the app shows its own banner from this instead of the built-in one.
10
+ */
11
+ function useWervtUpdate() {
12
+ return {
13
+ available: readonly(useUpdateAvailable()),
14
+ reload: () => reloadNuxtApp({
15
+ force: true,
16
+ persistState: true
17
+ })
18
+ };
19
+ }
20
+ //#endregion
21
+ export { useUpdateAvailable, useWervtUpdate };
@@ -3,6 +3,7 @@ import { useDb } from "./db.js";
3
3
  import { createEvlogError, log, useLogger } from "./logger.js";
4
4
  import { ShapeOptions, proxyShape } from "./shape.js";
5
5
  import { Task, TaskEvent, TaskResult, defineTask } from "./tasks.js";
6
- import { useWervtUser } from "./user.js";
7
6
  import { SafeResult, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
8
- export { type SafeResult, type ShapeOptions, type Task, type TaskEvent, type TaskResult, type WervtUser, createEvlogError, defineTask, log, proxyShape, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
7
+ import { Tool, defineTool } from "./tools.js";
8
+ import { useWervtUser } from "./user.js";
9
+ export { type SafeResult, type ShapeOptions, type Task, type TaskEvent, type TaskResult, type Tool, type WervtUser, createEvlogError, defineTask, defineTool, log, proxyShape, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
@@ -2,6 +2,7 @@ import { useDb } from "./db.js";
2
2
  import { createEvlogError, log, useLogger } from "./logger.js";
3
3
  import { proxyShape } from "./shape.js";
4
4
  import { defineTask } from "./tasks.js";
5
+ import { defineTool } from "./tools.js";
5
6
  import { useWervtUser } from "./user.js";
6
7
  import { useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
7
- export { createEvlogError, defineTask, log, proxyShape, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
8
+ export { createEvlogError, defineTask, defineTool, log, proxyShape, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
@@ -0,0 +1,32 @@
1
+ //#region src/runtime/server/tool-handlers.d.ts
2
+ /** `GET /_wervt/tools`: name, description and JSON Schema of each tool, for the client plugin. */
3
+ export declare const listTools: import("nuxt/server").EventHandler<Promise<{
4
+ name: string;
5
+ description: string;
6
+ inputSchema: import("@valibot/to-json-schema").JsonSchema | {
7
+ type: string;
8
+ properties: {};
9
+ };
10
+ readOnly: boolean;
11
+ }[]>>;
12
+ /** `POST /_wervt/tools/:name` with the arguments as JSON body: runs the tool's handler. */
13
+ export declare const runTool: import("nuxt/server").EventHandler<Promise<{
14
+ error: string;
15
+ issues?: undefined;
16
+ result?: undefined;
17
+ } | {
18
+ error: string;
19
+ issues: {
20
+ readonly root?: [string, ...string[]] | undefined;
21
+ readonly nested?: {
22
+ readonly [x: string]: [string, ...string[]] | undefined;
23
+ } | undefined;
24
+ readonly other?: [string, ...string[]] | undefined;
25
+ };
26
+ result?: undefined;
27
+ } | {
28
+ result: {} | null;
29
+ error?: undefined;
30
+ issues?: undefined;
31
+ }>>;
32
+ //#endregion
@@ -0,0 +1,70 @@
1
+ import { useLogger } from "./logger.js";
2
+ import { safeValidate, toSchema } from "./validate.js";
3
+ import { defineEventHandler } from "nuxt/server";
4
+ import { toJsonSchema } from "@valibot/to-json-schema";
5
+ import { tools } from "#wervt/tools";
6
+ //#region src/runtime/server/tool-handlers.ts
7
+ async function load(name) {
8
+ return (await (Object.hasOwn(tools, name) ? tools[name] : void 0)?.())?.default;
9
+ }
10
+ function inputSchema(tool) {
11
+ if (!tool.input) return {
12
+ type: "object",
13
+ properties: {}
14
+ };
15
+ const schema = toJsonSchema(toSchema(tool.input), {
16
+ typeMode: "input",
17
+ errorMode: "ignore"
18
+ });
19
+ delete schema.$schema;
20
+ return schema;
21
+ }
22
+ /** `GET /_wervt/tools`: name, description and JSON Schema of each tool, for the client plugin. */
23
+ const listTools = defineEventHandler(async (event) => {
24
+ event.res.headers.set("cache-control", "no-store");
25
+ return (await Promise.all(Object.keys(tools).map(async (name) => {
26
+ const tool = await load(name);
27
+ if (!tool) return void 0;
28
+ return {
29
+ name,
30
+ description: tool.description,
31
+ inputSchema: inputSchema(tool),
32
+ readOnly: tool.readOnly === true
33
+ };
34
+ }))).filter((tool) => tool !== void 0);
35
+ });
36
+ function crossSite(event) {
37
+ const site = event.req.headers.get("sec-fetch-site");
38
+ return site !== null && site !== "same-origin";
39
+ }
40
+ /** `POST /_wervt/tools/:name` with the arguments as JSON body: runs the tool's handler. */
41
+ const runTool = defineEventHandler(async (event) => {
42
+ event.res.headers.set("cache-control", "no-store");
43
+ if (crossSite(event)) {
44
+ event.res.status = 403;
45
+ return { error: "Tools can only be called from the app itself." };
46
+ }
47
+ const name = decodeURIComponent(new URL(event.req.url).pathname.split("/").pop() ?? "");
48
+ const tool = await load(name);
49
+ if (!tool) {
50
+ event.res.status = 404;
51
+ return { error: `No tool named ${name}.` };
52
+ }
53
+ useLogger(event).set({ tool: name });
54
+ const body = await event.req.json().catch(() => void 0);
55
+ let input;
56
+ if (tool.input) {
57
+ const result = await safeValidate(tool.input, body ?? {});
58
+ if (!result.success) {
59
+ event.res.status = 400;
60
+ return {
61
+ error: "Invalid arguments.",
62
+ issues: result.issues
63
+ };
64
+ }
65
+ input = result.data;
66
+ }
67
+ return { result: await tool.handler(event, input) ?? null };
68
+ });
69
+ //#endregion
70
+ export { listTools, runTool };
@@ -0,0 +1,2 @@
1
+ import { listTools } from "./tool-handlers.js";
2
+ export { listTools as default };
@@ -0,0 +1,5 @@
1
+ import { listTools } from "./tool-handlers.js";
2
+ //#region src/runtime/server/tool-list.ts
3
+ var tool_list_default = listTools;
4
+ //#endregion
5
+ export { tool_list_default as default };
@@ -0,0 +1,2 @@
1
+ import { runTool } from "./tool-handlers.js";
2
+ export { runTool as default };
@@ -0,0 +1,5 @@
1
+ import { runTool } from "./tool-handlers.js";
2
+ //#region src/runtime/server/tool-run.ts
3
+ var tool_run_default = runTool;
4
+ //#endregion
5
+ export { tool_run_default as default };
@@ -0,0 +1,15 @@
1
+ import { Input, Output } from "./validate.js";
2
+ import { RequestEvent } from "nuxt/server";
3
+ //#region src/runtime/server/tools.d.ts
4
+ export interface Tool<I extends Input | undefined = Input | undefined, R = unknown> {
5
+ /** What the tool does and when an agent should call it. Agents read this to pick a tool. */
6
+ description: string;
7
+ /** valibot schema or `{ key: schema }` shape of the arguments. Omit it for no arguments. */
8
+ input?: I;
9
+ /** Only reads, changes nothing. Agents may call such tools without asking the user. */
10
+ readOnly?: boolean;
11
+ handler(event: RequestEvent, input: I extends Input ? Output<I> : undefined): R | Promise<R>;
12
+ }
13
+ /** A tool in `server/tools/` (`server/tools/todos/add.ts` is `todos.add`). */
14
+ export declare function defineTool<R, I extends Input | undefined = undefined>(tool: Tool<I, R>): Tool<I, R>;
15
+ //#endregion
@@ -0,0 +1,7 @@
1
+ //#region src/runtime/server/tools.ts
2
+ /** A tool in `server/tools/` (`server/tools/todos/add.ts` is `todos.add`). */
3
+ function defineTool(tool) {
4
+ return tool;
5
+ }
6
+ //#endregion
7
+ export { defineTool };
@@ -3,8 +3,8 @@ import * as v from "valibot";
3
3
  //#region src/runtime/server/validate.d.ts
4
4
  type Schema = v.GenericSchema | v.GenericSchemaAsync;
5
5
  type Shape = v.ObjectEntriesAsync;
6
- type Input = Schema | Shape;
7
- type Output<T extends Input> = T extends Schema ? v.InferOutput<T> : T extends Shape ? v.InferOutput<v.ObjectSchemaAsync<T, undefined>> : never;
6
+ export type Input = Schema | Shape;
7
+ export type Output<T extends Input> = T extends Schema ? v.InferOutput<T> : T extends Shape ? v.InferOutput<v.ObjectSchemaAsync<T, undefined>> : never;
8
8
  export type SafeResult<T extends Input> = {
9
9
  success: true;
10
10
  data: Output<T>;
@@ -14,6 +14,8 @@ export type SafeResult<T extends Input> = {
14
14
  data?: undefined;
15
15
  issues: v.FlatErrors<undefined>;
16
16
  };
17
+ export declare function toSchema(input: Input): Schema;
18
+ export declare function safeValidate<T extends Input>(input: T, data: unknown): Promise<SafeResult<T>>;
17
19
  /** Parsed JSON body. Throws a 400 with flattened issues when invalid. */
18
20
  export declare const useValidatedBody: <T extends Input>(event: RequestEvent, schema: T) => Promise<Output<T>>;
19
21
  /** Query parameters (strings; use `vh.*` to coerce). Throws a 400 when invalid. */
@@ -48,4 +48,4 @@ const vh = {
48
48
  id: v.pipe(v.string(), v.digits(), v.transform(Number), v.safeInteger(), v.minValue(1))
49
49
  };
50
50
  //#endregion
51
- export { useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh };
51
+ export { safeValidate, toSchema, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wervt/nuxt",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Nuxt module for apps hosted on wervt: database, live data and runtime setup.",
5
5
  "keywords": [
6
6
  "drizzle",
@@ -39,6 +39,7 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@electric-sql/client": "^1.5.28",
42
+ "@valibot/to-json-schema": "^1.8.0",
42
43
  "aws4fetch": "^1.0.20",
43
44
  "evlog": "^2.30.0",
44
45
  "postgres": "^3.4.9",