@wervt/nuxt 0.2.12 → 0.2.14
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 +3 -1
- package/dist/module.d.ts +36 -0
- package/dist/module.js +68 -8
- package/dist/node_modules/.pnpm/nitro@3.0.260903-beta/node_modules/nitro/dist/runtime/internal/task.js +1 -1
- package/dist/runtime/app/plugins/feedback.client.d.ts +4 -0
- package/dist/runtime/app/plugins/feedback.client.js +32 -0
- package/dist/runtime/app/useWervtFeedback.d.ts +24 -0
- package/dist/runtime/app/useWervtFeedback.js +21 -0
- package/dist/runtime/server/index.d.ts +2 -1
- package/dist/runtime/server/mcp-auth-server.d.ts +4 -0
- package/dist/runtime/server/mcp-auth-server.js +21 -0
- package/dist/runtime/server/mcp-auth.d.ts +59 -0
- package/dist/runtime/server/mcp-auth.js +98 -0
- package/dist/runtime/server/mcp-claims.d.ts +5 -0
- package/dist/runtime/server/mcp-claims.js +18 -0
- package/dist/runtime/server/mcp-metadata.d.ts +4 -0
- package/dist/runtime/server/mcp-metadata.js +14 -0
- package/dist/runtime/server/mcp.d.ts +4 -0
- package/dist/runtime/server/mcp.js +112 -0
- package/dist/runtime/server/task-run-nitro2.d.ts +1 -1
- package/dist/runtime/server/tool-handlers.d.ts +7 -0
- package/dist/runtime/server/tool-handlers.js +7 -7
- package/dist/runtime/server/user.d.ts +1 -1
- package/dist/runtime/server/user.js +3 -1
- package/package.json +12 -2
- package/dist/node_modules/.pnpm/h3@2.0.2_crossws@0.4.15_srvx@1.0.5__ocache@0.3.0/node_modules/h3/dist/response.js +0 -96
package/README.md
CHANGED
|
@@ -20,7 +20,9 @@ export default defineNuxtConfig({
|
|
|
20
20
|
- `useWervtUser()`: the signed-in wervt user, on the server and in the app.
|
|
21
21
|
- `useValidatedBody()` / `useValidatedQuery()` / `useValidatedParams()`: valibot validation.
|
|
22
22
|
- `useWervtTrack()`: custom events for wervt's cookieless analytics (page views are counted automatically).
|
|
23
|
-
- `
|
|
23
|
+
- `wervt: { feedback: { key } }` and `useWervtFeedback()`: the kontor feedback widget, reports go into a kontor project's triage.
|
|
24
|
+
- `defineTool()` (server): tools in `server/tools/` that browser agents call through WebMCP, and
|
|
25
|
+
MCP clients (Claude Code, Cursor, …) at `/mcp`, signed in through wervt's OAuth login.
|
|
24
26
|
- `nuxt dev` starts against the local wervt stack (Postgres, Electric) and a dev user.
|
|
25
27
|
|
|
26
28
|
Deploy with [`@wervt/cli`](https://www.npmjs.com/package/@wervt/cli): `wervt deploy`.
|
package/dist/module.d.ts
CHANGED
|
@@ -18,12 +18,48 @@ export interface ModuleOptions {
|
|
|
18
18
|
* Defaults to true; `false` leaves them out of the build.
|
|
19
19
|
*/
|
|
20
20
|
webmcp?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Serves the tools in `server/tools/` to MCP clients (Claude Code, Cursor, …) at `/mcp`,
|
|
23
|
+
* signed in through wervt's login. Defaults to true; `false` turns it off. An object names
|
|
24
|
+
* the server and tells clients how to use the tools (`instructions`).
|
|
25
|
+
*/
|
|
26
|
+
mcp?: boolean | McpOptions;
|
|
21
27
|
/**
|
|
22
28
|
* Reports client-side navigations and `useWervtTrack()` events to wervt's usage analytics, so
|
|
23
29
|
* page views count more than the first load. Defaults to true. Cookieless; page loads are
|
|
24
30
|
* counted by the runtime either way.
|
|
25
31
|
*/
|
|
26
32
|
analytics?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The kontor feedback widget: a "Feedback" button that reports issues into a kontor project's
|
|
35
|
+
* triage, as the signed-in wervt user when the app's URL is an allowed origin of the key.
|
|
36
|
+
* With `true`, the key comes from `wervt env <app> --set NUXT_PUBLIC_WERVT_FEEDBACK_KEY=pk_…`
|
|
37
|
+
* (it can override a configured key the same way).
|
|
38
|
+
*/
|
|
39
|
+
feedback?: FeedbackOptions | boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface FeedbackOptions {
|
|
42
|
+
/** Public intake key of the kontor project (Project settings → Intake keys). */
|
|
43
|
+
key?: string;
|
|
44
|
+
/** The kontor instance. Defaults to https://kontor.wervt.app. */
|
|
45
|
+
url?: string;
|
|
46
|
+
/** Widget language. Defaults to the browser's. */
|
|
47
|
+
lang?: 'de' | 'en';
|
|
48
|
+
/** `none` hides the floating button; open the form with `useWervtFeedback().open()`. */
|
|
49
|
+
position?: 'bottom-right' | 'bottom-left' | 'none';
|
|
50
|
+
label?: string;
|
|
51
|
+
/** Accent color (any CSS color). */
|
|
52
|
+
color?: string;
|
|
53
|
+
/** Also load it in `nuxt dev`. Off by default: localhost usually isn't an allowed origin. */
|
|
54
|
+
dev?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface McpOptions {
|
|
57
|
+
/** Server name clients show. Defaults to the app name. */
|
|
58
|
+
name?: string;
|
|
59
|
+
title?: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
/** How to use the tools together, e.g. which one to call first. Clients pass it to the model. */
|
|
62
|
+
instructions?: string;
|
|
27
63
|
}
|
|
28
64
|
/** `server/wervt-tasks.json` in the build: the app's tasks and schedules, for the control plane. */
|
|
29
65
|
export interface TasksManifest {
|
package/dist/module.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolveAppName, setupDev } from "./dev.js";
|
|
2
2
|
import { addImports, addPlugin, addServerHandler, addServerPlugin, addServerTemplate, createResolver, defineNuxtModule, getNuxtVersion, hasNuxtModule, installModule, useLogger } from "nuxt/kit";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
-
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
5
5
|
import { builtinModules, createRequire } from "node:module";
|
|
6
6
|
import { dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
7
7
|
import evlog from "evlog/nuxt";
|
|
@@ -163,12 +163,18 @@ var module_default = defineNuxtModule({
|
|
|
163
163
|
});
|
|
164
164
|
}
|
|
165
165
|
const toolsDir = join(nuxt.options.serverDir, "tools");
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
166
|
+
const webmcp = options.webmcp !== false;
|
|
167
|
+
let mcp = options.mcp !== false;
|
|
168
|
+
const tools = webmcp || mcp ? await findTools(toolsDir) : [];
|
|
169
|
+
if (tools.length > 0 && mcp && nitroMajor < 3) {
|
|
170
|
+
if (options.mcp) logger.warn("`/mcp` needs Nuxt 5; this app keeps its tools for WebMCP only.");
|
|
171
|
+
mcp = false;
|
|
172
|
+
}
|
|
173
|
+
if (tools.length > 0) addServerTemplate({
|
|
174
|
+
filename: "#wervt/tools",
|
|
175
|
+
getContents: () => `export const tools = {\n${tools.map(({ name, path }) => ` ${JSON.stringify(name)}: () => import(${JSON.stringify(path)}),`).join("\n")}\n}\n`
|
|
176
|
+
});
|
|
177
|
+
if (tools.length > 0 && webmcp) {
|
|
172
178
|
addServerHandler({
|
|
173
179
|
route: "/_wervt/tools",
|
|
174
180
|
method: "get",
|
|
@@ -184,7 +190,34 @@ var module_default = defineNuxtModule({
|
|
|
184
190
|
mode: "client"
|
|
185
191
|
});
|
|
186
192
|
}
|
|
187
|
-
if (
|
|
193
|
+
if (tools.length > 0 && mcp) {
|
|
194
|
+
const config = typeof options.mcp === "object" ? options.mcp : {};
|
|
195
|
+
const app = await resolveAppName(nuxt.options.rootDir, options.app);
|
|
196
|
+
const server = {
|
|
197
|
+
name: config.name ?? app,
|
|
198
|
+
version: await appVersion(nuxt.options.rootDir),
|
|
199
|
+
title: config.title,
|
|
200
|
+
description: config.description,
|
|
201
|
+
instructions: config.instructions
|
|
202
|
+
};
|
|
203
|
+
addServerTemplate({
|
|
204
|
+
filename: "#wervt/mcp",
|
|
205
|
+
getContents: () => `export const server = ${JSON.stringify(server)}\n`
|
|
206
|
+
});
|
|
207
|
+
addServerHandler({
|
|
208
|
+
route: "/mcp",
|
|
209
|
+
handler: resolver.resolve("./runtime/server/mcp")
|
|
210
|
+
});
|
|
211
|
+
addServerHandler({
|
|
212
|
+
route: "/.well-known/oauth-protected-resource/mcp",
|
|
213
|
+
handler: resolver.resolve("./runtime/server/mcp-metadata")
|
|
214
|
+
});
|
|
215
|
+
addServerHandler({
|
|
216
|
+
route: "/.well-known/oauth-authorization-server",
|
|
217
|
+
handler: resolver.resolve("./runtime/server/mcp-auth-server")
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (nuxt.options.dev && (webmcp || mcp)) nuxt.hook("builder:watch", async (event, path) => {
|
|
188
221
|
const file = relative(toolsDir, resolve(nuxt.options.srcDir, path));
|
|
189
222
|
if ((event === "add" || event === "unlink") && !file.startsWith("..")) await nuxt.callHook("restart");
|
|
190
223
|
});
|
|
@@ -232,6 +265,24 @@ var module_default = defineNuxtModule({
|
|
|
232
265
|
mode: "client"
|
|
233
266
|
});
|
|
234
267
|
}
|
|
268
|
+
const feedbackOptions = options.feedback === true ? {} : options.feedback;
|
|
269
|
+
if (feedbackOptions && (feedbackOptions.dev || !nuxt.options.dev)) {
|
|
270
|
+
const { dev: _, ...feedback } = feedbackOptions;
|
|
271
|
+
const config = nuxt.options.runtimeConfig.public;
|
|
272
|
+
config.wervt = {
|
|
273
|
+
...config.wervt,
|
|
274
|
+
feedback: {
|
|
275
|
+
key: "",
|
|
276
|
+
url: "https://kontor.wervt.app",
|
|
277
|
+
...feedback,
|
|
278
|
+
app: await resolveAppName(nuxt.options.rootDir, options.app)
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
addPlugin({
|
|
282
|
+
src: resolver.resolve("./runtime/app/plugins/feedback.client"),
|
|
283
|
+
mode: "client"
|
|
284
|
+
});
|
|
285
|
+
}
|
|
235
286
|
nuxt.options.build.transpile.push(resolver.resolve("./runtime"));
|
|
236
287
|
addImports([
|
|
237
288
|
{
|
|
@@ -257,6 +308,10 @@ var module_default = defineNuxtModule({
|
|
|
257
308
|
{
|
|
258
309
|
name: "useWervtUpdate",
|
|
259
310
|
from: resolver.resolve("./runtime/app/useWervtUpdate")
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: "useWervtFeedback",
|
|
314
|
+
from: resolver.resolve("./runtime/app/useWervtFeedback")
|
|
260
315
|
}
|
|
261
316
|
]);
|
|
262
317
|
}
|
|
@@ -288,6 +343,11 @@ async function findTools(dir) {
|
|
|
288
343
|
return tools;
|
|
289
344
|
}
|
|
290
345
|
const logger = useLogger("wervt");
|
|
346
|
+
/** The app's package.json version, which MCP clients see as the server version. */
|
|
347
|
+
async function appVersion(rootDir) {
|
|
348
|
+
const pkg = JSON.parse(await readFile(join(rootDir, "package.json"), "utf8").catch(() => "{}"));
|
|
349
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
350
|
+
}
|
|
291
351
|
/** Whether `name` is in a `node_modules` folder Node would look in from `dir`. */
|
|
292
352
|
function hasPackage(dir, name) {
|
|
293
353
|
for (let current = dir;; current = dirname(current)) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { HTTPError } from "../../../../../../h3@2.0.2_crossws@0.4.15_srvx@1.0.5__ocache@0.3.0/node_modules/h3/dist/response.js";
|
|
2
1
|
import { tasks } from "#nitro/virtual/tasks";
|
|
2
|
+
import { HTTPError } from "h3";
|
|
3
3
|
//#region ../../node_modules/.pnpm/nitro@3.0.260903-beta/node_modules/nitro/dist/runtime/internal/task.mjs
|
|
4
4
|
const __runningTasks__ = {};
|
|
5
5
|
async function runTask(name, { payload = {}, context = {} } = {}) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { pendingFeedbackContext } from "../useWervtFeedback.js";
|
|
2
|
+
import { defineNuxtPlugin, onNuxtReady, useRuntimeConfig } from "#app";
|
|
3
|
+
//#region src/runtime/app/plugins/feedback.client.ts
|
|
4
|
+
var feedback_client_default = defineNuxtPlugin({
|
|
5
|
+
name: "wervt:feedback",
|
|
6
|
+
setup() {
|
|
7
|
+
const config = useRuntimeConfig();
|
|
8
|
+
const feedback = config.public.wervt?.feedback;
|
|
9
|
+
if (!feedback?.key) return;
|
|
10
|
+
onNuxtReady(() => {
|
|
11
|
+
const script = document.createElement("script");
|
|
12
|
+
script.src = `${feedback.url.replace(/\/$/, "")}/widget.js`;
|
|
13
|
+
script.async = true;
|
|
14
|
+
script.dataset.key = feedback.key;
|
|
15
|
+
for (const name of [
|
|
16
|
+
"lang",
|
|
17
|
+
"position",
|
|
18
|
+
"label",
|
|
19
|
+
"color"
|
|
20
|
+
]) if (feedback[name]) script.dataset[name] = feedback[name];
|
|
21
|
+
script.dataset.context = JSON.stringify({
|
|
22
|
+
app: feedback.app,
|
|
23
|
+
release: config.app.buildId,
|
|
24
|
+
...pendingFeedbackContext
|
|
25
|
+
});
|
|
26
|
+
script.addEventListener("load", () => window.kontor?.setContext(pendingFeedbackContext));
|
|
27
|
+
document.head.appendChild(script);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
//#endregion
|
|
32
|
+
export { feedback_client_default as default };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/runtime/app/useWervtFeedback.d.ts
|
|
2
|
+
interface KontorWidget {
|
|
3
|
+
open(): void;
|
|
4
|
+
close(): void;
|
|
5
|
+
setContext(values: Record<string, unknown>): void;
|
|
6
|
+
}
|
|
7
|
+
declare global {
|
|
8
|
+
interface Window {
|
|
9
|
+
kontor?: KontorWidget;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export declare const pendingFeedbackContext: Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Controls the feedback widget (`wervt: { feedback: { key } }`): open it from your own button,
|
|
15
|
+
* or add context to every report, e.g. what the user was looking at.
|
|
16
|
+
*
|
|
17
|
+
* @example useWervtFeedback().setContext({ room: room.id })
|
|
18
|
+
*/
|
|
19
|
+
export declare function useWervtFeedback(): {
|
|
20
|
+
open: () => void | undefined;
|
|
21
|
+
close: () => void | undefined;
|
|
22
|
+
setContext(values: Record<string, unknown>): void;
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/runtime/app/useWervtFeedback.ts
|
|
2
|
+
const pendingFeedbackContext = {};
|
|
3
|
+
/**
|
|
4
|
+
* Controls the feedback widget (`wervt: { feedback: { key } }`): open it from your own button,
|
|
5
|
+
* or add context to every report, e.g. what the user was looking at.
|
|
6
|
+
*
|
|
7
|
+
* @example useWervtFeedback().setContext({ room: room.id })
|
|
8
|
+
*/
|
|
9
|
+
function useWervtFeedback() {
|
|
10
|
+
const widget = () => import.meta.client ? window.kontor : void 0;
|
|
11
|
+
return {
|
|
12
|
+
open: () => widget()?.open(),
|
|
13
|
+
close: () => widget()?.close(),
|
|
14
|
+
setContext(values) {
|
|
15
|
+
Object.assign(pendingFeedbackContext, values);
|
|
16
|
+
widget()?.setContext(values);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { pendingFeedbackContext, useWervtFeedback };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { WervtUser } from "../shared/user.js";
|
|
2
2
|
import { useDb } from "./db.js";
|
|
3
3
|
import { createEvlogError, log, useLogger } from "./logger.js";
|
|
4
|
+
import { McpTokenContext } from "./mcp-auth.js";
|
|
4
5
|
import { ShapeOptions, proxyShape } from "./shape.js";
|
|
5
6
|
import { Task, TaskEvent, TaskResult, defineTask } from "./tasks.js";
|
|
6
7
|
import { SafeResult, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
|
|
7
8
|
import { Tool, defineTool } from "./tools.js";
|
|
8
9
|
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 };
|
|
10
|
+
export { type McpTokenContext, 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 };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CORS, fetchAuth, mcpEndpoint } from "./mcp-auth.js";
|
|
2
|
+
import { defineEventHandler } from "nuxt/server";
|
|
3
|
+
//#region src/runtime/server/mcp-auth-server.ts
|
|
4
|
+
var mcp_auth_server_default = defineEventHandler(async (event) => {
|
|
5
|
+
const endpoint = mcpEndpoint(event);
|
|
6
|
+
if (event.req.method === "OPTIONS") return new Response(null, {
|
|
7
|
+
status: 204,
|
|
8
|
+
headers: CORS
|
|
9
|
+
});
|
|
10
|
+
if (!endpoint) return Response.json({ error: "No login configured." }, { status: 404 });
|
|
11
|
+
const issuer = new URL(endpoint.issuer);
|
|
12
|
+
const url = new URL(`/.well-known/oauth-authorization-server${issuer.pathname}`, issuer);
|
|
13
|
+
const res = await fetchAuth(url.href);
|
|
14
|
+
if (!res.ok) return Response.json({ error: "authorization_server_unavailable" }, {
|
|
15
|
+
status: 502,
|
|
16
|
+
headers: CORS
|
|
17
|
+
});
|
|
18
|
+
return Response.json(await res.json(), { headers: CORS });
|
|
19
|
+
});
|
|
20
|
+
//#endregion
|
|
21
|
+
export { mcp_auth_server_default as default };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { WervtUser } from "../shared/user.js";
|
|
2
|
+
import { RequestEvent } from "nuxt/server";
|
|
3
|
+
//#region src/runtime/server/mcp-auth.d.ts
|
|
4
|
+
/** What `wervt:mcp:token` handlers get: set `user` to accept the token. */
|
|
5
|
+
export interface McpTokenContext {
|
|
6
|
+
token: string;
|
|
7
|
+
event: RequestEvent;
|
|
8
|
+
user?: WervtUser;
|
|
9
|
+
}
|
|
10
|
+
declare module 'nuxt/schema' {
|
|
11
|
+
interface NuxtServerHooks {
|
|
12
|
+
/**
|
|
13
|
+
* A bearer token on `/mcp` that isn't a wervt access token, e.g. the app's own API token
|
|
14
|
+
* for scripts. Set `ctx.user` to accept it; tools then see that user in `useWervtUser`.
|
|
15
|
+
*/
|
|
16
|
+
'wervt:mcp:token': (ctx: McpTokenContext) => void | Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export interface McpEndpoint {
|
|
20
|
+
/** This app's `/mcp`, the audience its access tokens must carry. */
|
|
21
|
+
resource: string;
|
|
22
|
+
/** wervt's login as OAuth issuer, e.g. `https://auth.wervt.app/api/auth`. */
|
|
23
|
+
issuer: string;
|
|
24
|
+
/** RFC 9728 metadata of `resource`, where the 401 points MCP clients. */
|
|
25
|
+
metadataUrl: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Where this app's `/mcp` and wervt's login live, from the runtime's `WERVT_APP_URL` and
|
|
29
|
+
* `WERVT_AUTH_URL`. Undefined without a login: `nuxt dev` then serves `/mcp` as the dev user.
|
|
30
|
+
*/
|
|
31
|
+
export declare function mcpEndpoint(event: RequestEvent): McpEndpoint | undefined;
|
|
32
|
+
/** Discovery documents are public; browser-based MCP clients read them cross-origin. */
|
|
33
|
+
export declare const CORS: {
|
|
34
|
+
'access-control-allow-origin': string;
|
|
35
|
+
'access-control-allow-methods': string;
|
|
36
|
+
'access-control-allow-headers': string;
|
|
37
|
+
'access-control-max-age': string;
|
|
38
|
+
};
|
|
39
|
+
/** RFC 9728: tells MCP clients which login issues tokens for `/mcp`. */
|
|
40
|
+
export declare function protectedResourceMetadata(endpoint: McpEndpoint): {
|
|
41
|
+
resource: string;
|
|
42
|
+
authorization_servers: string[];
|
|
43
|
+
bearer_methods_supported: string[];
|
|
44
|
+
scopes_supported: string[];
|
|
45
|
+
resource_name: string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Fetches from wervt's login. Inside wervt it's another isolate on the same runtime, so the
|
|
49
|
+
* request goes to the runtime directly (named by `x-wervt-host`): no round trip through the
|
|
50
|
+
* internet, and `*.localhost` of the local stack doesn't resolve inside the runtime anyway.
|
|
51
|
+
*/
|
|
52
|
+
export declare function fetchAuth(issuerUrl: string, init?: RequestInit): Promise<Response>;
|
|
53
|
+
/** Verifies a wervt access token for `endpoint` and returns its user. */
|
|
54
|
+
export declare function verifyAccessToken(token: string, endpoint: McpEndpoint): Promise<WervtUser | undefined>;
|
|
55
|
+
/** The user a bearer token on `/mcp` stands for: a wervt access token, or the app's own. */
|
|
56
|
+
export declare function authenticateMcp(token: string, event: RequestEvent, endpoint: McpEndpoint): Promise<WervtUser | undefined>;
|
|
57
|
+
/** Makes `useWervtUser(event)` return the token's user while a tool runs over MCP. */
|
|
58
|
+
export declare function setMcpUser(event: RequestEvent, user: WervtUser): void;
|
|
59
|
+
//#endregion
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { userFromClaims } from "./mcp-claims.js";
|
|
2
|
+
import { getRequestURL, useServerHooks } from "nuxt/server";
|
|
3
|
+
import { createRemoteJWKSet, customFetch, errors, jwtVerify } from "jose";
|
|
4
|
+
//#region src/runtime/server/mcp-auth.ts
|
|
5
|
+
const trimSlash = (url) => url.replace(/\/+$/, "");
|
|
6
|
+
/**
|
|
7
|
+
* Where this app's `/mcp` and wervt's login live, from the runtime's `WERVT_APP_URL` and
|
|
8
|
+
* `WERVT_AUTH_URL`. Undefined without a login: `nuxt dev` then serves `/mcp` as the dev user.
|
|
9
|
+
*/
|
|
10
|
+
function mcpEndpoint(event) {
|
|
11
|
+
const authUrl = process.env.WERVT_AUTH_URL;
|
|
12
|
+
if (!authUrl) return void 0;
|
|
13
|
+
const appUrl = trimSlash(process.env.WERVT_APP_URL ?? getRequestURL(event).origin);
|
|
14
|
+
return {
|
|
15
|
+
resource: `${appUrl}/mcp`,
|
|
16
|
+
issuer: `${trimSlash(authUrl)}/api/auth`,
|
|
17
|
+
metadataUrl: `${appUrl}/.well-known/oauth-protected-resource/mcp`
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Discovery documents are public; browser-based MCP clients read them cross-origin. */
|
|
21
|
+
const CORS = {
|
|
22
|
+
"access-control-allow-origin": "*",
|
|
23
|
+
"access-control-allow-methods": "GET, OPTIONS",
|
|
24
|
+
"access-control-allow-headers": "*",
|
|
25
|
+
"access-control-max-age": "86400"
|
|
26
|
+
};
|
|
27
|
+
/** RFC 9728: tells MCP clients which login issues tokens for `/mcp`. */
|
|
28
|
+
function protectedResourceMetadata(endpoint) {
|
|
29
|
+
return {
|
|
30
|
+
resource: endpoint.resource,
|
|
31
|
+
authorization_servers: [endpoint.issuer],
|
|
32
|
+
bearer_methods_supported: ["header"],
|
|
33
|
+
scopes_supported: [
|
|
34
|
+
"openid",
|
|
35
|
+
"profile",
|
|
36
|
+
"email",
|
|
37
|
+
"offline_access"
|
|
38
|
+
],
|
|
39
|
+
resource_name: new URL(endpoint.resource).hostname.split(".")[0]
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Fetches from wervt's login. Inside wervt it's another isolate on the same runtime, so the
|
|
44
|
+
* request goes to the runtime directly (named by `x-wervt-host`): no round trip through the
|
|
45
|
+
* internet, and `*.localhost` of the local stack doesn't resolve inside the runtime anyway.
|
|
46
|
+
*/
|
|
47
|
+
function fetchAuth(issuerUrl, init = {}) {
|
|
48
|
+
const runtime = process.env.WERVT_RUNTIME_URL;
|
|
49
|
+
if (!runtime) return fetch(issuerUrl, init);
|
|
50
|
+
const url = new URL(issuerUrl);
|
|
51
|
+
const headers = new Headers(init.headers);
|
|
52
|
+
headers.set("x-wervt-host", url.host);
|
|
53
|
+
return fetch(new URL(url.pathname + url.search, runtime), {
|
|
54
|
+
...init,
|
|
55
|
+
headers
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const keySets = /* @__PURE__ */ new Map();
|
|
59
|
+
function keySet(issuer) {
|
|
60
|
+
let keys = keySets.get(issuer);
|
|
61
|
+
if (!keys) {
|
|
62
|
+
keys = createRemoteJWKSet(new URL(`${issuer}/jwks`), { [customFetch]: (url, init) => fetchAuth(url, init) });
|
|
63
|
+
keySets.set(issuer, keys);
|
|
64
|
+
}
|
|
65
|
+
return keys;
|
|
66
|
+
}
|
|
67
|
+
/** Verifies a wervt access token for `endpoint` and returns its user. */
|
|
68
|
+
async function verifyAccessToken(token, endpoint) {
|
|
69
|
+
try {
|
|
70
|
+
const { payload } = await jwtVerify(token, keySet(endpoint.issuer), {
|
|
71
|
+
issuer: endpoint.issuer,
|
|
72
|
+
audience: endpoint.resource,
|
|
73
|
+
requiredClaims: ["exp", "sub"],
|
|
74
|
+
clockTolerance: 5
|
|
75
|
+
});
|
|
76
|
+
return userFromClaims(payload);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error instanceof errors.JOSEError && !(error instanceof errors.JWKSTimeout)) return void 0;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** The user a bearer token on `/mcp` stands for: a wervt access token, or the app's own. */
|
|
83
|
+
async function authenticateMcp(token, event, endpoint) {
|
|
84
|
+
const user = await verifyAccessToken(token, endpoint);
|
|
85
|
+
if (user) return user;
|
|
86
|
+
const ctx = {
|
|
87
|
+
token,
|
|
88
|
+
event
|
|
89
|
+
};
|
|
90
|
+
await useServerHooks().callHook("wervt:mcp:token", ctx);
|
|
91
|
+
return ctx.user;
|
|
92
|
+
}
|
|
93
|
+
/** Makes `useWervtUser(event)` return the token's user while a tool runs over MCP. */
|
|
94
|
+
function setMcpUser(event, user) {
|
|
95
|
+
event.context.wervtMcpUser = user;
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
export { CORS, authenticateMcp, fetchAuth, mcpEndpoint, protectedResourceMetadata, setMcpUser, verifyAccessToken };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { WervtUser } from "../shared/user.js";
|
|
2
|
+
//#region src/runtime/server/mcp-claims.d.ts
|
|
3
|
+
/** The wervt user an access token's claims name, or undefined when they're incomplete. */
|
|
4
|
+
export declare function userFromClaims(claims: Record<string, unknown>): WervtUser | undefined;
|
|
5
|
+
//#endregion
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/runtime/server/mcp-claims.ts
|
|
2
|
+
/** The wervt user an access token's claims name, or undefined when they're incomplete. */
|
|
3
|
+
function userFromClaims(claims) {
|
|
4
|
+
const { sub, email, name, picture, accounts } = claims;
|
|
5
|
+
if (typeof sub !== "string" || !sub || typeof email !== "string") return void 0;
|
|
6
|
+
return {
|
|
7
|
+
id: sub,
|
|
8
|
+
email: email.toLowerCase(),
|
|
9
|
+
name: typeof name === "string" ? name : void 0,
|
|
10
|
+
image: typeof picture === "string" ? picture : void 0,
|
|
11
|
+
accounts: isAccounts(accounts) ? accounts : void 0
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function isAccounts(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((id) => typeof id === "string");
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { userFromClaims };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CORS, mcpEndpoint, protectedResourceMetadata } from "./mcp-auth.js";
|
|
2
|
+
import { defineEventHandler } from "nuxt/server";
|
|
3
|
+
//#region src/runtime/server/mcp-metadata.ts
|
|
4
|
+
var mcp_metadata_default = defineEventHandler((event) => {
|
|
5
|
+
const endpoint = mcpEndpoint(event);
|
|
6
|
+
if (event.req.method === "OPTIONS") return new Response(null, {
|
|
7
|
+
status: 204,
|
|
8
|
+
headers: CORS
|
|
9
|
+
});
|
|
10
|
+
if (!endpoint) return Response.json({ error: "No login configured." }, { status: 404 });
|
|
11
|
+
return Response.json(protectedResourceMetadata(endpoint), { headers: CORS });
|
|
12
|
+
});
|
|
13
|
+
//#endregion
|
|
14
|
+
export { mcp_metadata_default as default };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { devUser } from "../shared/user.js";
|
|
2
|
+
import { useLogger } from "./logger.js";
|
|
3
|
+
import { safeValidate } from "./validate.js";
|
|
4
|
+
import { authenticateMcp, mcpEndpoint, setMcpUser } from "./mcp-auth.js";
|
|
5
|
+
import { loadTool, toolInputSchema } from "./tool-handlers.js";
|
|
6
|
+
import { defineEventHandler } from "nuxt/server";
|
|
7
|
+
import { server } from "#wervt/mcp";
|
|
8
|
+
import { tools } from "#wervt/tools";
|
|
9
|
+
import { defineMcpHandler } from "h3-mcp";
|
|
10
|
+
//#region src/runtime/server/mcp.ts
|
|
11
|
+
const MAX_BODY = 4194304;
|
|
12
|
+
const text = (value) => [{
|
|
13
|
+
type: "text",
|
|
14
|
+
text: value
|
|
15
|
+
}];
|
|
16
|
+
/** What the client sees of a thrown error: its status and message, like the WebMCP route. */
|
|
17
|
+
function errorResult(error) {
|
|
18
|
+
const { status, message, data } = error ?? {};
|
|
19
|
+
let body = message || "Tool failed.";
|
|
20
|
+
if (typeof status === "number") body = `[${status}] ${body}`;
|
|
21
|
+
if (data !== void 0) body += `\n${JSON.stringify(data)}`;
|
|
22
|
+
return {
|
|
23
|
+
content: text(body),
|
|
24
|
+
isError: true
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function toMcpTool(name, tool) {
|
|
28
|
+
return {
|
|
29
|
+
name,
|
|
30
|
+
description: tool.description,
|
|
31
|
+
inputSchema: toolInputSchema(tool),
|
|
32
|
+
annotations: { readOnlyHint: tool.readOnly === true },
|
|
33
|
+
async handler(args, event) {
|
|
34
|
+
const requestEvent = event;
|
|
35
|
+
useLogger(requestEvent).set({
|
|
36
|
+
tool: name,
|
|
37
|
+
mcp: true
|
|
38
|
+
});
|
|
39
|
+
let input;
|
|
40
|
+
if (tool.input) {
|
|
41
|
+
const result = await safeValidate(tool.input, args ?? {});
|
|
42
|
+
if (!result.success) return {
|
|
43
|
+
content: text(`Invalid arguments.\n${JSON.stringify(result.issues)}`),
|
|
44
|
+
isError: true
|
|
45
|
+
};
|
|
46
|
+
input = result.data;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const result = await tool.handler(requestEvent, input);
|
|
50
|
+
return { content: result === void 0 ? [] : text(JSON.stringify(result)) };
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return errorResult(error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
let handler;
|
|
58
|
+
async function createHandler() {
|
|
59
|
+
const definitions = (await Promise.all(Object.keys(tools).map(async (name) => {
|
|
60
|
+
const tool = await loadTool(name);
|
|
61
|
+
return tool ? toMcpTool(name, tool) : void 0;
|
|
62
|
+
}))).filter((tool) => tool !== void 0);
|
|
63
|
+
return defineMcpHandler((event) => ({
|
|
64
|
+
...server,
|
|
65
|
+
tools: definitions,
|
|
66
|
+
origin: mcpEndpoint(event) ? false : {}
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
function unauthorized(metadataUrl, invalid) {
|
|
70
|
+
const params = [`resource_metadata="${metadataUrl}"`];
|
|
71
|
+
if (invalid) params.push("error=\"invalid_token\"");
|
|
72
|
+
return new Response(JSON.stringify({ error: "Sign in with wervt to use this app over MCP." }), {
|
|
73
|
+
status: 401,
|
|
74
|
+
headers: {
|
|
75
|
+
"content-type": "application/json",
|
|
76
|
+
"www-authenticate": `Bearer ${params.join(", ")}`,
|
|
77
|
+
"cache-control": "no-store"
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
var mcp_default = defineEventHandler(async (event) => {
|
|
82
|
+
const endpoint = mcpEndpoint(event);
|
|
83
|
+
if (endpoint) {
|
|
84
|
+
const authorization = event.req.headers.get("authorization") ?? "";
|
|
85
|
+
const token = /^bearer\s+(\S+)$/i.exec(authorization)?.[1];
|
|
86
|
+
const user = token ? await authenticateMcp(token, event, endpoint) : void 0;
|
|
87
|
+
if (!user) return unauthorized(endpoint.metadataUrl, Boolean(token));
|
|
88
|
+
setMcpUser(event, user);
|
|
89
|
+
} else if (import.meta.dev) {
|
|
90
|
+
const user = devUser(true);
|
|
91
|
+
if (user) setMcpUser(event, user);
|
|
92
|
+
} else return Response.json({ error: "MCP needs WERVT_AUTH_URL, which the wervt runtime sets." }, { status: 503 });
|
|
93
|
+
if (event.req.method === "POST") {
|
|
94
|
+
if (Number(event.req.headers.get("content-length") ?? 0) > MAX_BODY) return Response.json({ error: "Body too large." }, { status: 413 });
|
|
95
|
+
const body = await event.req.arrayBuffer();
|
|
96
|
+
if (body.byteLength > MAX_BODY) return Response.json({ error: "Body too large." }, { status: 413 });
|
|
97
|
+
const h3Event = event;
|
|
98
|
+
h3Event.req = new Request(event.req.url, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: event.req.headers,
|
|
101
|
+
body,
|
|
102
|
+
signal: event.req.signal
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
handler ??= createHandler().catch((error) => {
|
|
106
|
+
handler = void 0;
|
|
107
|
+
throw error;
|
|
108
|
+
});
|
|
109
|
+
return (await handler)(event);
|
|
110
|
+
});
|
|
111
|
+
//#endregion
|
|
112
|
+
export { mcp_default as default };
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { Input } from "./validate.js";
|
|
2
|
+
import { Tool } from "./tools.js";
|
|
1
3
|
//#region src/runtime/server/tool-handlers.d.ts
|
|
4
|
+
export declare function loadTool(name: string): Promise<Tool<Input | undefined, unknown> | undefined>;
|
|
5
|
+
export declare function toolInputSchema(tool: Tool): import("@valibot/to-json-schema").JsonSchema | {
|
|
6
|
+
type: string;
|
|
7
|
+
properties: {};
|
|
8
|
+
};
|
|
2
9
|
/** `GET /_wervt/tools`: name, description and JSON Schema of each tool, for the client plugin. */
|
|
3
10
|
export declare const listTools: import("nuxt/server").EventHandler<Promise<{
|
|
4
11
|
name: string;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { useLogger } from "./logger.js";
|
|
2
2
|
import { safeValidate, toSchema } from "./validate.js";
|
|
3
3
|
import { defineEventHandler } from "nuxt/server";
|
|
4
|
-
import { toJsonSchema } from "@valibot/to-json-schema";
|
|
5
4
|
import { tools } from "#wervt/tools";
|
|
5
|
+
import { toJsonSchema } from "@valibot/to-json-schema";
|
|
6
6
|
//#region src/runtime/server/tool-handlers.ts
|
|
7
|
-
async function
|
|
7
|
+
async function loadTool(name) {
|
|
8
8
|
return (await (Object.hasOwn(tools, name) ? tools[name] : void 0)?.())?.default;
|
|
9
9
|
}
|
|
10
|
-
function
|
|
10
|
+
function toolInputSchema(tool) {
|
|
11
11
|
if (!tool.input) return {
|
|
12
12
|
type: "object",
|
|
13
13
|
properties: {}
|
|
@@ -23,12 +23,12 @@ function inputSchema(tool) {
|
|
|
23
23
|
const listTools = defineEventHandler(async (event) => {
|
|
24
24
|
event.res.headers.set("cache-control", "no-store");
|
|
25
25
|
return (await Promise.all(Object.keys(tools).map(async (name) => {
|
|
26
|
-
const tool = await
|
|
26
|
+
const tool = await loadTool(name);
|
|
27
27
|
if (!tool) return void 0;
|
|
28
28
|
return {
|
|
29
29
|
name,
|
|
30
30
|
description: tool.description,
|
|
31
|
-
inputSchema:
|
|
31
|
+
inputSchema: toolInputSchema(tool),
|
|
32
32
|
readOnly: tool.readOnly === true
|
|
33
33
|
};
|
|
34
34
|
}))).filter((tool) => tool !== void 0);
|
|
@@ -45,7 +45,7 @@ const runTool = defineEventHandler(async (event) => {
|
|
|
45
45
|
return { error: "Tools can only be called from the app itself." };
|
|
46
46
|
}
|
|
47
47
|
const name = decodeURIComponent(new URL(event.req.url).pathname.split("/").pop() ?? "");
|
|
48
|
-
const tool = await
|
|
48
|
+
const tool = await loadTool(name);
|
|
49
49
|
if (!tool) {
|
|
50
50
|
event.res.status = 404;
|
|
51
51
|
return { error: `No tool named ${name}.` };
|
|
@@ -67,4 +67,4 @@ const runTool = defineEventHandler(async (event) => {
|
|
|
67
67
|
return { result: await tool.handler(event, input) ?? null };
|
|
68
68
|
});
|
|
69
69
|
//#endregion
|
|
70
|
-
export { listTools, runTool };
|
|
70
|
+
export { listTools, loadTool, runTool, toolInputSchema };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { DEV_LOGIN_COOKIE, WervtUser, parseWervtUser } from "../shared/user.js";
|
|
2
|
-
import "./user.js";
|
|
3
2
|
import { RequestEvent } from "nuxt/server";
|
|
4
3
|
//#region src/runtime/server/user.d.ts
|
|
5
4
|
/**
|
|
6
5
|
* The signed-in wervt user, or `undefined` when nobody is signed in.
|
|
7
6
|
* Private apps always have one (the runtime checked the login and the allow-list); public
|
|
8
7
|
* apps get one when the visitor is signed in. Send people to `/_wervt/login` to sign in.
|
|
8
|
+
* In a tool called over MCP, it's the user the client signed in as.
|
|
9
9
|
*/
|
|
10
10
|
export declare const useWervtUser: (event: RequestEvent) => WervtUser | undefined;
|
|
11
11
|
//#endregion
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { DEV_LOGIN_COOKIE, devUser, parseWervtUser } from "../shared/user.js";
|
|
2
2
|
import { getCookie } from "nuxt/server";
|
|
3
3
|
//#region src/runtime/server/user.ts
|
|
4
|
+
const MCP_USER = "wervtMcpUser";
|
|
4
5
|
/**
|
|
5
6
|
* The signed-in wervt user, or `undefined` when nobody is signed in.
|
|
6
7
|
* Private apps always have one (the runtime checked the login and the allow-list); public
|
|
7
8
|
* apps get one when the visitor is signed in. Send people to `/_wervt/login` to sign in.
|
|
9
|
+
* In a tool called over MCP, it's the user the client signed in as.
|
|
8
10
|
*/
|
|
9
|
-
const useWervtUser = (event) => parseWervtUser(event.req.headers.get("x-wervt-user")) ?? devUser(getCookie(event, "wervt_dev_login") === "1");
|
|
11
|
+
const useWervtUser = (event) => event.context[MCP_USER] ?? parseWervtUser(event.req.headers.get("x-wervt-user")) ?? devUser(getCookie(event, "wervt_dev_login") === "1");
|
|
10
12
|
//#endregion
|
|
11
13
|
export { DEV_LOGIN_COOKIE, parseWervtUser, useWervtUser };
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wervt/nuxt",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Nuxt module for apps hosted on wervt: database, live data and runtime setup.",
|
|
3
|
+
"version": "0.2.14",
|
|
4
|
+
"description": "Nuxt module for apps hosted on wervt: database, live data, MCP and runtime setup.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"drizzle",
|
|
7
7
|
"electric",
|
|
8
|
+
"mcp",
|
|
8
9
|
"nuxt",
|
|
9
10
|
"nuxt-module",
|
|
10
11
|
"postgres",
|
|
@@ -42,19 +43,28 @@
|
|
|
42
43
|
"@valibot/to-json-schema": "^1.8.0",
|
|
43
44
|
"aws4fetch": "^1.0.20",
|
|
44
45
|
"evlog": "^2.30.1",
|
|
46
|
+
"h3-mcp": "0.2.0",
|
|
47
|
+
"jose": "^6.2.12",
|
|
45
48
|
"postgres": "^3.4.9",
|
|
46
49
|
"valibot": "^1.5.0"
|
|
47
50
|
},
|
|
48
51
|
"devDependencies": {
|
|
49
52
|
"drizzle-orm": "1.0.0-rc.4",
|
|
53
|
+
"h3": "2.0.2",
|
|
50
54
|
"nitro": "3.0.260903-beta",
|
|
51
55
|
"nuxt": "npm:nuxt-nightly@5x",
|
|
52
56
|
"vue": "^3.5.43"
|
|
53
57
|
},
|
|
54
58
|
"peerDependencies": {
|
|
55
59
|
"drizzle-orm": "1.0.0-rc.4",
|
|
60
|
+
"h3": "^2.0.1",
|
|
56
61
|
"nuxt": ">=4.2"
|
|
57
62
|
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"h3": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
58
68
|
"scripts": {
|
|
59
69
|
"build": "vp pack"
|
|
60
70
|
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
String.raw`%(?:2[146-9A-E]|3[0-9ABD]|4[0-9A-F]|5[0-9ABDF]|6[1-9A-F]|7[0-9ACE])`;
|
|
2
|
-
const DISALLOWED_STATUS_CHARS = /[^\u0009\u0020-\u007E]/g;
|
|
3
|
-
function sanitizeStatusMessage(statusMessage = "") {
|
|
4
|
-
return statusMessage.replace(DISALLOWED_STATUS_CHARS, "");
|
|
5
|
-
}
|
|
6
|
-
function sanitizeStatusCode(statusCode, defaultStatusCode = 200) {
|
|
7
|
-
if (!statusCode) return defaultStatusCode;
|
|
8
|
-
if (typeof statusCode === "string") statusCode = +statusCode;
|
|
9
|
-
if (!Number.isInteger(statusCode) || statusCode < 100 || statusCode > 599) return defaultStatusCode;
|
|
10
|
-
return statusCode;
|
|
11
|
-
}
|
|
12
|
-
var HTTPError = class HTTPError extends Error {
|
|
13
|
-
get name() {
|
|
14
|
-
return "HTTPError";
|
|
15
|
-
}
|
|
16
|
-
status;
|
|
17
|
-
statusText;
|
|
18
|
-
headers;
|
|
19
|
-
cause;
|
|
20
|
-
data;
|
|
21
|
-
body;
|
|
22
|
-
unhandled;
|
|
23
|
-
static isError(input) {
|
|
24
|
-
return input instanceof Error && input?.name === "HTTPError" && input.status > 99;
|
|
25
|
-
}
|
|
26
|
-
static status(status, statusText, details) {
|
|
27
|
-
return new HTTPError({
|
|
28
|
-
...details,
|
|
29
|
-
statusText,
|
|
30
|
-
status
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
constructor(arg1, arg2) {
|
|
34
|
-
let messageInput;
|
|
35
|
-
let details;
|
|
36
|
-
if (typeof arg1 === "string") {
|
|
37
|
-
messageInput = arg1;
|
|
38
|
-
details = arg2;
|
|
39
|
-
} else details = arg1;
|
|
40
|
-
const status = sanitizeStatusCode(details?.status || details?.statusCode || (details?.cause)?.status || (details?.cause)?.statusCode, 500);
|
|
41
|
-
const statusText = sanitizeStatusMessage(details?.statusText || details?.statusMessage || (details?.cause)?.statusText || (details?.cause)?.statusMessage);
|
|
42
|
-
const message = messageInput || details?.message || (details?.cause)?.message || details?.statusText || details?.statusMessage || [
|
|
43
|
-
"HTTPError",
|
|
44
|
-
status,
|
|
45
|
-
statusText
|
|
46
|
-
].filter(Boolean).join(" ");
|
|
47
|
-
super(message, { cause: details });
|
|
48
|
-
this.cause = details;
|
|
49
|
-
this.status = status;
|
|
50
|
-
this.statusText = statusText || void 0;
|
|
51
|
-
const rawHeaders = details?.headers || (details?.cause)?.headers;
|
|
52
|
-
this.headers = rawHeaders ? new Headers(rawHeaders) : void 0;
|
|
53
|
-
this.unhandled = details?.unhandled ?? (details?.cause)?.unhandled ?? void 0;
|
|
54
|
-
this.data = details?.data;
|
|
55
|
-
this.body = details?.body;
|
|
56
|
-
}
|
|
57
|
-
get statusCode() {
|
|
58
|
-
return this.status;
|
|
59
|
-
}
|
|
60
|
-
get statusMessage() {
|
|
61
|
-
return this.statusText;
|
|
62
|
-
}
|
|
63
|
-
toJSON() {
|
|
64
|
-
const unhandled = this.unhandled;
|
|
65
|
-
return {
|
|
66
|
-
status: this.status,
|
|
67
|
-
statusText: this.statusText,
|
|
68
|
-
unhandled,
|
|
69
|
-
message: unhandled ? "HTTPError" : this.message,
|
|
70
|
-
data: unhandled ? void 0 : this.data,
|
|
71
|
-
...unhandled ? void 0 : this.body
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
const kHTTPResponse = /* @__PURE__ */ Symbol.for("h3.HTTPResponse");
|
|
76
|
-
var HTTPResponse = class {
|
|
77
|
-
#headers;
|
|
78
|
-
#init;
|
|
79
|
-
body;
|
|
80
|
-
constructor(body, init) {
|
|
81
|
-
this.body = body;
|
|
82
|
-
this.#init = init;
|
|
83
|
-
}
|
|
84
|
-
get status() {
|
|
85
|
-
return this.#init?.status;
|
|
86
|
-
}
|
|
87
|
-
get statusText() {
|
|
88
|
-
return this.#init?.statusText;
|
|
89
|
-
}
|
|
90
|
-
get headers() {
|
|
91
|
-
return this.#headers ||= new Headers(this.#init?.headers);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
HTTPResponse.prototype[kHTTPResponse] = true;
|
|
95
|
-
//#endregion
|
|
96
|
-
export { HTTPError, HTTPResponse, sanitizeStatusCode, sanitizeStatusMessage };
|