@thednp/rpc 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +30 -15
- package/CHANGELOG.md +53 -0
- package/README.md +35 -29
- package/dist/express/express.d.mts +13 -1
- package/dist/express/express.d.mts.map +1 -1
- package/dist/express/express.mjs +51 -60
- package/dist/express/express.mjs.map +1 -1
- package/dist/fastify/fastify.d.mts +16 -1
- package/dist/fastify/fastify.d.mts.map +1 -1
- package/dist/fastify/fastify.mjs +40 -60
- package/dist/fastify/fastify.mjs.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.d.mts +20 -3
- package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.mjs +39 -59
- package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
- package/dist/h3/h3.d.mts +94 -0
- package/dist/h3/h3.d.mts.map +1 -0
- package/dist/h3/h3.mjs +245 -0
- package/dist/h3/h3.mjs.map +1 -0
- package/dist/helpers/helpers.d.mts +2 -0
- package/dist/helpers/helpers.d.mts.map +1 -1
- package/dist/helpers/helpers.mjs.map +1 -1
- package/dist/hono/hono.d.mts +14 -40
- package/dist/hono/hono.d.mts.map +1 -1
- package/dist/hono/hono.mjs +39 -71
- package/dist/hono/hono.mjs.map +1 -1
- package/dist/index.d.mts +36 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4 -0
- package/dist/index.mjs.map +1 -1
- package/dist/koa/koa.d.mts +12 -1
- package/dist/koa/koa.d.mts.map +1 -1
- package/dist/koa/koa.mjs +46 -61
- package/dist/koa/koa.mjs.map +1 -1
- package/dist/server/server.d.mts +98 -3
- package/dist/server/server.d.mts.map +1 -1
- package/dist/server/server.mjs +79 -1
- package/dist/server/server.mjs.map +1 -1
- package/package.json +10 -4
|
@@ -5,8 +5,10 @@ import { ViteDevServer } from "vite";
|
|
|
5
5
|
import "express";
|
|
6
6
|
import "hono";
|
|
7
7
|
import "@hono/node-server";
|
|
8
|
+
import "hono/utils/http-status";
|
|
8
9
|
import "hono/factory";
|
|
9
10
|
import "koa";
|
|
11
|
+
import "h3";
|
|
10
12
|
//#region src/fastify/types.d.ts
|
|
11
13
|
/**
|
|
12
14
|
* Fastify RPC plugin signature: registers the middleware as a preHandler hook.
|
|
@@ -76,6 +78,9 @@ type BodyResult = {
|
|
|
76
78
|
} | {
|
|
77
79
|
contentType: "text/plain";
|
|
78
80
|
data: string;
|
|
81
|
+
} | {
|
|
82
|
+
contentType: "application/x-www-form-urlencoded";
|
|
83
|
+
data: Record<string, unknown>;
|
|
79
84
|
} | {
|
|
80
85
|
contentType: "multipart/form-data";
|
|
81
86
|
data: Record<string, unknown>;
|
|
@@ -121,6 +126,16 @@ declare function attachVite(app: FastifyInstance, vite: ViteDevServer): void;
|
|
|
121
126
|
* @returns A promise resolving to the parsed body with its content type
|
|
122
127
|
*/
|
|
123
128
|
declare const readBody: (req: FastifyRequest) => Promise<BodyResult>;
|
|
129
|
+
/**
|
|
130
|
+
* Issues an HTTP redirect on a Fastify reply using the native
|
|
131
|
+
* `reply.redirect(location, status)` API (Fastify v5 signature: destination
|
|
132
|
+
* URL first, status code optional). Defaults to `303 See Other` for
|
|
133
|
+
* convention (Post/Redirect/Get).
|
|
134
|
+
* @param reply - Fastify reply object
|
|
135
|
+
* @param location - The URL to redirect to
|
|
136
|
+
* @param status - HTTP status code, defaults to 303
|
|
137
|
+
*/
|
|
138
|
+
declare const redirect: (reply: FastifyReply, location: string, status?: number) => void;
|
|
124
139
|
//#endregion
|
|
125
|
-
export { type FastifyMiddlewareFn, type FastifyMiddlewareHooks, type FastifyMiddlewareOptions, type FastifyPlugin, type FastifyRPCPlugin, type RegisteredFastifyRPCPlugin, type RpcFastifyPluginOptions, attachRPC, attachVite, createMiddleware, createRPCMiddleware, readBody };
|
|
140
|
+
export { type FastifyMiddlewareFn, type FastifyMiddlewareHooks, type FastifyMiddlewareOptions, type FastifyPlugin, type FastifyRPCPlugin, type RegisteredFastifyRPCPlugin, type RpcFastifyPluginOptions, attachRPC, attachVite, createMiddleware, createRPCMiddleware, readBody, redirect };
|
|
126
141
|
//# sourceMappingURL=fastify.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fastify.d.mts","names":[],"sources":["../../src/fastify/types.d.ts","../../src/fastify/createMiddleware.ts","../../src/types.d.ts","../../src/fastify/helpers.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"fastify.d.mts","names":[],"sources":["../../src/fastify/types.d.ts","../../src/fastify/createMiddleware.ts","../../src/types.d.ts","../../src/fastify/helpers.ts"],"mappings":";;;;;;;;;;;;;;;KAYY,oBACV,SAAS,iBACT,gBAAgB,QAAQ,+BACxB;;;;KAMU,uBAAuB;;;;KAKvB,6BAA6B,WAAW;;;;KAKxC,0BAA0B;;EAEpC;;;;;KAMU,2BAA2B;;;;;KAM3B,uBACV,UAAU,yCAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,gBACL,KAAK,cACL,MAAM,4BACH;;;;;;;;;;cCzBM,kBAAkB;;;;;;;;cAgFlB,qBAAqB;;;;;;KCxCtB;EACN;EAAiC,MAAM;;EACvC;EAA2B;;EAE7B;EACA,MAAM;;EAEJ;EAAoC,MAAM;;;;;;KA8BpC;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,aAAa,WAAW;;;;KAIxB,YAAY,gBAAgB,YAAY;;;;;;;;iBCpH9B,UAAU,KAAK,kBAAe;;;;;;;iBAepC,WAAW,KAAK,iBAAiB,MAAM;;;;;;;cAgB1C,WAAQ,KACd,mBACJ,QAAQ;;;;;;;;;;cAoFE,WAAQ,OACZ,cAAY,kBACH"}
|
package/dist/fastify/fastify.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
|
|
1
|
+
import { escapeRegExp, formatError, hasContentTypeMismatch, provideRequestContext, scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
|
|
2
2
|
import fp from "fastify-plugin";
|
|
3
3
|
//#region src/options.ts
|
|
4
4
|
const defaultRPCOptions = {
|
|
@@ -13,66 +13,15 @@ const defaultMiddlewareOptions = {
|
|
|
13
13
|
origin: void 0
|
|
14
14
|
};
|
|
15
15
|
//#endregion
|
|
16
|
-
//#region src/tools.ts
|
|
17
|
-
/**
|
|
18
|
-
* Escapes special regex metacharacters in a string.
|
|
19
|
-
* Used to safely embed user-configurable values (like rpcPrefix) into regular expressions,
|
|
20
|
-
* preventing ReDoS and regex injection attacks.
|
|
21
|
-
* @param s - The raw string to escape
|
|
22
|
-
* @returns The escaped string safe for use in new RegExp()
|
|
23
|
-
*/
|
|
24
|
-
function escapeRegExp(s) {
|
|
25
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
26
|
-
}
|
|
27
|
-
//#endregion
|
|
28
16
|
//#region src/constants.ts
|
|
29
17
|
const FUNCTION_NOT_FOUND = "Function not found";
|
|
30
18
|
const METHOD_NOT_ALLOWED = "Method Not Allowed";
|
|
31
19
|
const REQUEST_FORBIDDEN = "Forbidden";
|
|
32
|
-
const
|
|
20
|
+
const UNSUPPORTED_MEDIA_TYPE = "Unsupported Media Type";
|
|
33
21
|
const CLIENT_DISCONNECTED = "client disconnected";
|
|
34
22
|
/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */
|
|
35
23
|
const MIDDLEWARE_NAME_USED = (name) => `The middleware name "${name}" is already used.`;
|
|
36
24
|
//#endregion
|
|
37
|
-
//#region src/server-helpers.ts
|
|
38
|
-
/**
|
|
39
|
-
* A typed error thrown from server functions.
|
|
40
|
-
* The middleware serializes the `message` and `code` in the response,
|
|
41
|
-
* allowing clients to recognise and handle specific error conditions.
|
|
42
|
-
*/
|
|
43
|
-
var RPCError = class extends Error {
|
|
44
|
-
/** Machine-readable error code (e.g. "VALIDATION_FAILED", "UNAUTHORIZED") */
|
|
45
|
-
code;
|
|
46
|
-
/** Optional diagnostic payload */
|
|
47
|
-
data;
|
|
48
|
-
constructor(message, code = "INTERNAL", data) {
|
|
49
|
-
super(message);
|
|
50
|
-
this.name = "RPCError";
|
|
51
|
-
this.code = code;
|
|
52
|
-
this.data = data;
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
/**
|
|
56
|
-
* Formats an error for the RPC middleware response.
|
|
57
|
-
* In development the full `RPCError` payload is included so developers
|
|
58
|
-
* can quickly identify issues. Unexpected exceptions never expose their
|
|
59
|
-
* message — only the generic "Internal Server Error" is sent, preventing
|
|
60
|
-
* information disclosure; server-side diagnostics are preserved via the
|
|
61
|
-
* middleware's `console.error` logging.
|
|
62
|
-
*/
|
|
63
|
-
const formatError = (err, isProduction) => {
|
|
64
|
-
if (isProduction) return { error: INTERNAL_SERVER_ERROR };
|
|
65
|
-
if (err instanceof RPCError) {
|
|
66
|
-
const payload = {
|
|
67
|
-
error: err.message || "Internal Server Error",
|
|
68
|
-
code: err.code
|
|
69
|
-
};
|
|
70
|
-
if (err.data !== void 0) payload.data = err.data;
|
|
71
|
-
return payload;
|
|
72
|
-
}
|
|
73
|
-
return { error: INTERNAL_SERVER_ERROR };
|
|
74
|
-
};
|
|
75
|
-
//#endregion
|
|
76
25
|
//#region src/fastify/plugin.ts
|
|
77
26
|
/** @module Fastify plugin. Exports the RPC plugin wrapped with `fastify-plugin` for lifecycle-compatible registration. */
|
|
78
27
|
const RpcPlugin = (fastify, initialOptions, done) => {
|
|
@@ -125,9 +74,10 @@ const readBody = (req) => {
|
|
|
125
74
|
if (reqBody !== void 0) {
|
|
126
75
|
const isJSON = contentType.includes("json");
|
|
127
76
|
const isMultipart = contentType.includes("multipart/form-data");
|
|
77
|
+
const isUrlEncoded = contentType.includes("urlencoded");
|
|
128
78
|
resolve({
|
|
129
|
-
contentType: isMultipart ? "multipart/form-data" : isJSON ? "application/json" : "text/plain",
|
|
130
|
-
data: isMultipart ? reqBody : isJSON ? reqBody : String(reqBody)
|
|
79
|
+
contentType: isMultipart ? "multipart/form-data" : isJSON ? "application/json" : isUrlEncoded ? "application/x-www-form-urlencoded" : "text/plain",
|
|
80
|
+
data: isMultipart ? reqBody : isJSON ? reqBody : isUrlEncoded ? reqBody : String(reqBody)
|
|
131
81
|
});
|
|
132
82
|
return;
|
|
133
83
|
}
|
|
@@ -145,10 +95,11 @@ const readBody = (req) => {
|
|
|
145
95
|
toggleListeners();
|
|
146
96
|
const isJSON = contentType.includes("json");
|
|
147
97
|
const isMultipart = contentType.includes("multipart/form-data");
|
|
98
|
+
const isUrlEncoded = contentType.includes("urlencoded");
|
|
148
99
|
try {
|
|
149
|
-
const data = isMultipart ? { raw: body } : JSON.parse(body);
|
|
100
|
+
const data = isMultipart ? { raw: body } : isUrlEncoded ? Object.fromEntries(new URLSearchParams(body)) : JSON.parse(body);
|
|
150
101
|
resolve({
|
|
151
|
-
contentType: isMultipart ? "multipart/form-data" : isJSON ? "application/json" : "text/plain",
|
|
102
|
+
contentType: isMultipart ? "multipart/form-data" : isJSON ? "application/json" : isUrlEncoded ? "application/x-www-form-urlencoded" : "text/plain",
|
|
152
103
|
data: isMultipart ? data : data
|
|
153
104
|
});
|
|
154
105
|
} catch (_e) {
|
|
@@ -165,6 +116,18 @@ const readBody = (req) => {
|
|
|
165
116
|
toggleListeners(true);
|
|
166
117
|
});
|
|
167
118
|
};
|
|
119
|
+
/**
|
|
120
|
+
* Issues an HTTP redirect on a Fastify reply using the native
|
|
121
|
+
* `reply.redirect(location, status)` API (Fastify v5 signature: destination
|
|
122
|
+
* URL first, status code optional). Defaults to `303 See Other` for
|
|
123
|
+
* convention (Post/Redirect/Get).
|
|
124
|
+
* @param reply - Fastify reply object
|
|
125
|
+
* @param location - The URL to redirect to
|
|
126
|
+
* @param status - HTTP status code, defaults to 303
|
|
127
|
+
*/
|
|
128
|
+
const redirect = (reply, location, status = 303) => {
|
|
129
|
+
reply.redirect(location, status);
|
|
130
|
+
};
|
|
168
131
|
//#endregion
|
|
169
132
|
//#region src/fastify/createMiddleware.ts
|
|
170
133
|
let middlewareCount = 0;
|
|
@@ -251,15 +214,32 @@ const createRPCMiddleware = (initialOptions = {}) => {
|
|
|
251
214
|
const raw = reqUrl.searchParams.get("args");
|
|
252
215
|
if (raw) args = JSON.parse(raw);
|
|
253
216
|
} else {
|
|
217
|
+
if (hasContentTypeMismatch(serverFunction.options?.contentType ?? "application/json", req.headers["content-type"])) {
|
|
218
|
+
reply.status(415).send({ error: UNSUPPORTED_MEDIA_TYPE });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
254
221
|
const body = await readBody(req);
|
|
255
222
|
args = Array.isArray(body.data) ? body.data : [body.data];
|
|
256
223
|
}
|
|
257
|
-
const
|
|
224
|
+
const requestEvent = {
|
|
225
|
+
request: req,
|
|
226
|
+
response: reply,
|
|
227
|
+
nativeEvent: req,
|
|
228
|
+
locals: {},
|
|
229
|
+
redirect: (location, status = 303) => {
|
|
230
|
+
requestEvent.redirected = {
|
|
231
|
+
location,
|
|
232
|
+
status
|
|
233
|
+
};
|
|
234
|
+
redirect(reply, location, status);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const { data: dataResult, cancel } = provideRequestContext(requestEvent, () => serverFunction.handler(...args));
|
|
258
238
|
const onClose = () => cancel(CLIENT_DISCONNECTED);
|
|
259
239
|
req.raw.on("close", onClose);
|
|
260
240
|
const data = await dataResult;
|
|
261
241
|
req.raw.off("close", onClose);
|
|
262
|
-
if (!reply.raw.headersSent) reply.status(200).send({ data });
|
|
242
|
+
if (!requestEvent.redirected && !reply.raw.headersSent) reply.status(200).send({ data });
|
|
263
243
|
} catch (err) {
|
|
264
244
|
console.error(String(err));
|
|
265
245
|
const isProduction = process.env.NODE_ENV === "production";
|
|
@@ -269,6 +249,6 @@ const createRPCMiddleware = (initialOptions = {}) => {
|
|
|
269
249
|
});
|
|
270
250
|
};
|
|
271
251
|
//#endregion
|
|
272
|
-
export { attachRPC, attachVite, createMiddleware, createRPCMiddleware, readBody };
|
|
252
|
+
export { attachRPC, attachVite, createMiddleware, createRPCMiddleware, readBody, redirect };
|
|
273
253
|
|
|
274
254
|
//# sourceMappingURL=fastify.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fastify.mjs","names":["fastifyRpcPlugin"],"sources":["../../src/options.ts","../../src/tools.ts","../../src/constants.ts","../../src/server-helpers.ts","../../src/fastify/plugin.ts","../../src/fastify/helpers.ts","../../src/fastify/createMiddleware.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions = {\n contentType: \"application/json\",\n credentials: \"same-origin\",\n method: \"POST\",\n} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: \"__rpc\",\n adapter: \"express\",\n serverFiles: \"exact\",\n scanRoot: undefined,\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n} satisfies MiddlewareOptions;\n","/**\n * Escapes special regex metacharacters in a string.\n * Used to safely embed user-configurable values (like rpcPrefix) into regular expressions,\n * preventing ReDoS and regex injection attacks.\n * @param s - The raw string to escape\n * @returns The escaped string safe for use in new RegExp()\n */\nexport function escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n","export const OPERATION_ABORTED = \"Operation aborted\";\n\nexport const REQUEST_CANCELLED = \"Request was cancelled\";\n\nexport const FETCH_ERROR_PREFIX = \"Fetch error: \";\n\nexport const NO_SERVER_FUNCTION_FOUND = \"No server function found.\";\n\nexport const ERROR_LOADING_FILE = \"Error loading file:\";\n\nexport const FUNCTION_NOT_FOUND = \"Function not found\";\n\nexport const METHOD_NOT_ALLOWED = \"Method Not Allowed\";\n\nexport const REQUEST_FORBIDDEN = \"Forbidden\";\n\nexport const INTERNAL_SERVER_ERROR = \"Internal Server Error\";\n\nexport const CLIENT_DISCONNECTED = \"client disconnected\";\n\n/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */\nexport const MIDDLEWARE_NAME_USED = (name: string) =>\n `The middleware name \"${name}\" is already used.`;\n\n/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */\nexport const INVALID_IDENTIFIER = (label: string, name: string) =>\n `Invalid ${label}: \"${name}\" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;\n\n/** Error message when a value fails the safe-path-segment validation. @param label - What kind of value was being validated. @param segment - The rejected value */\nexport const INVALID_PATH_SEGMENT = (label: string, segment: string) =>\n `Invalid ${label}: \"${segment}\" must match /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/`;\n\n/** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */\nexport const CONFIG_FILE_NOT_FOUND = (\n configFile: string,\n configFilePath: string,\n) =>\n ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;\n\nexport const NO_CONFIG_FOUND = ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n\n/** Error template for duplicate server function names across files. @param name - The duplicate registered name */\nexport const DUPLICATE_FUNCTION_NAME = (name: string) =>\n `Duplicate server function \"${name}\" detected. Each server function must have a unique name. Remove or rename the duplicate.`;\n","/** @module Server-side utilities. Exports the `RPCError` class for typed server-side errors, `formatError` for middleware error responses, and `walkGlobFiles` for recursively discovering `*.server.*` files. Never import this module in client code — it is server-only. */\nimport type { JsonObject, JsonValue } from \"./types.d.ts\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { INTERNAL_SERVER_ERROR } from \"./constants.ts\";\n\nconst GLOB_REGEX = /^.+\\.server\\.(ts|js|mjs|mts)$/;\n\n/**\n * Recursively walks `dir` and collects absolute paths to files whose\n * basename matches the `*.server.{ts,js,mjs,mts}` glob pattern.\n */\nexport const walkGlobFiles = async (dir: string): Promise<string[]> => {\n const results: string[] = [];\n const stack = [dir];\n while (stack.length) {\n const current = stack.pop()!;\n let entries;\n try {\n entries = await readdir(current, { withFileTypes: true });\n } catch (_e) {\n continue;\n }\n for (const entry of entries) {\n const fullPath = join(current, entry.name);\n if (entry.isFile() && GLOB_REGEX.test(entry.name)) {\n results.push(fullPath);\n } else if (entry.isDirectory()) {\n stack.push(fullPath);\n }\n }\n }\n return results;\n};\n\n/**\n * A typed error thrown from server functions.\n * The middleware serializes the `message` and `code` in the response,\n * allowing clients to recognise and handle specific error conditions.\n */\nexport class RPCError extends Error {\n /** Machine-readable error code (e.g. \"VALIDATION_FAILED\", \"UNAUTHORIZED\") */\n code: string;\n /** Optional diagnostic payload */\n data?: JsonValue;\n constructor(message: string, code = \"INTERNAL\", data?: JsonValue) {\n super(message);\n this.name = \"RPCError\";\n this.code = code;\n this.data = data;\n }\n}\n\n/**\n * Formats an error for the RPC middleware response.\n * In development the full `RPCError` payload is included so developers\n * can quickly identify issues. Unexpected exceptions never expose their\n * message — only the generic \"Internal Server Error\" is sent, preventing\n * information disclosure; server-side diagnostics are preserved via the\n * middleware's `console.error` logging.\n */\nexport const formatError = (\n err: unknown,\n isProduction: boolean,\n): JsonObject => {\n if (isProduction) {\n return { error: INTERNAL_SERVER_ERROR };\n }\n if (err instanceof RPCError) {\n const payload: JsonObject = {\n error: err.message || INTERNAL_SERVER_ERROR,\n code: err.code,\n };\n if (err.data !== undefined) payload.data = err.data;\n return payload;\n }\n return { error: INTERNAL_SERVER_ERROR };\n};\n","/** @module Fastify plugin. Exports the RPC plugin wrapped with `fastify-plugin` for lifecycle-compatible registration. */\n// inspired by https://github.com/royalswe/vike-fastify-boilerplate/blob/main/server/index.ts\nimport fp from \"fastify-plugin\";\nimport type { MiddlewareOptions } from \"../types.d.ts\";\nimport type {\n FastifyRPCPlugin,\n RegisteredFastifyRPCPlugin,\n} from \"./types.d.ts\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\nexport type { MiddlewareOptions };\n\nconst RpcPlugin: FastifyRPCPlugin = (\n fastify,\n initialOptions,\n done,\n) => {\n // Register RPC middleware as preHandler hook\n const rpcMiddleware = createRPCMiddleware(initialOptions);\n fastify.addHook(\"preHandler\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n rpcMiddleware(request, reply, resolve);\n });\n await next();\n });\n\n done();\n};\n\n// Export the plugin wrapped with fastify-plugin\nconst rpcPlugin = fp(RpcPlugin, {\n name: \"uni-rpc-fastify-plugin\",\n}) as RegisteredFastifyRPCPlugin;\n\nexport { rpcPlugin as default };\n","// @thednp/rpc/src/fastify/helpers.ts\nimport type { FastifyRequest } from \"fastify\";\nimport type { ViteDevServer } from \"vite\";\nimport type { FastifyInstance } from \"fastify\";\nimport type { Buffer } from \"node:buffer\";\nimport type { BodyResult, JsonValue } from \"../types.d.ts\";\nimport fastifyRpcPlugin from \"./plugin.ts\";\n\n/**\n * Convenience function to load RPC config and register the RPC plugin to a Fastify instance.\n * Dynamically imports loadRPCConfig and registers the fastify-rpc plugin.\n * @param app - Fastify instance\n */\nexport async function attachRPC(app: FastifyInstance) {\n // The main plugin entry statically imports Vite, so loadRPCConfig is\n // imported lazily: function bundles that never call attachRPC (e.g.\n // serverless functions) keep Vite out of the bundle (or externalized).\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n await app.register(fastifyRpcPlugin, options);\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Fastify instance for development mode.\n * Uses an `onRequest` hook to delegate to Vite's connect-compatible middleware stack.\n * @param app - Fastify instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: FastifyInstance, vite: ViteDevServer) {\n app.addHook(\"onRequest\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n vite.middlewares(request.raw, reply.raw, resolve);\n });\n await next();\n });\n}\n\n/**\n * Reads and parses the HTTP request body from a Fastify request.\n * If Fastify's body parser already consumed the stream, uses the pre-parsed body from `req.body`.\n * @param req - Fastify request object\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n req: FastifyRequest,\n): Promise<BodyResult> => {\n return new Promise((resolve, reject) => {\n const contentType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const reqBody = req.body as JsonValue;\n\n if (reqBody !== undefined) {\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : \"text/plain\",\n data: isMultipart\n ? (reqBody as Record<string, unknown>)\n : isJSON\n ? reqBody\n : String(reqBody),\n } as BodyResult);\n return;\n }\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n req.raw[method](\"data\", onData);\n req.raw[method](\"end\", onEnd);\n req.raw[method](\"error\", onError);\n };\n\n let body = \"\";\n\n const onData = (chunk: Buffer) => {\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n try {\n const data = isMultipart ? { raw: body } : JSON.parse(body);\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : \"text/plain\",\n data: isMultipart ? (data as Record<string, unknown>) : data,\n } as BodyResult);\n } catch (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n const onError = (err: Error) => {\n toggleListeners();\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n","// src/fastify/createMiddleware.ts\nimport type {\n FastifyReply,\n FastifyRequest,\n HookHandlerDoneFunction,\n} from \"fastify\";\nimport type {\n FastifyMiddlewareFn,\n FastifyMiddlewareOptions,\n} from \"./types.d.ts\";\nimport type { JsonValue } from \"../types.d.ts\";\nimport { scanForServerFiles, serverFunctionsMap } from \"@thednp/rpc/server\";\nimport { defaultMiddlewareOptions, defaultRPCOptions } from \"../options.ts\";\nimport { escapeRegExp } from \"../tools.ts\";\nimport { formatError } from \"../server-helpers.ts\";\nimport {\n CLIENT_DISCONNECTED,\n FUNCTION_NOT_FOUND,\n METHOD_NOT_ALLOWED,\n MIDDLEWARE_NAME_USED,\n REQUEST_FORBIDDEN,\n} from \"../constants.ts\";\nimport { readBody } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Fastify preHandler hook with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Fastify preHandler hook function\n */\nexport const createMiddleware: FastifyMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n const middlewareName = options.name;\n const rpcPrefix = options.rpcPrefix;\n const path = options.path;\n const handler = options.handler;\n\n let name = middlewareName;\n if (!name) {\n name = \"viteRPCMiddleware-\" + middlewareCount;\n middlewareCount += 1;\n }\n if (middlewareStack.has(name)) {\n throw new Error(MIDDLEWARE_NAME_USED(name));\n }\n middlewareStack.add(name);\n\n // Hoist regex compilation out of per-request path. Escape the prefix to\n // prevent regex injection via metacharacters in the config string.\n const prefixRegex: RegExp | null = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : null;\n const pathMatcher: RegExp | null = path\n ? (typeof path === \"string\" ? new RegExp(path) : path)\n : null;\n\n const middlewareHandler = async (\n req: FastifyRequest,\n reply: FastifyReply,\n done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = new URL(req.url, \"http://localhost\");\n const url = reqUrl.pathname;\n\n // When serving from production server, scan for server files\n if (serverFunctionsMap.size === 0) {\n await scanForServerFiles();\n }\n\n // No need to continue when no handler provided\n if (!handler) {\n done();\n return;\n }\n\n // Path matching\n if (pathMatcher && !pathMatcher.test(url)) {\n done();\n return;\n }\n\n // rpcPrefix matching (boundary-safe via escaped regex)\n if (prefixRegex && !prefixRegex.test(url)) {\n done();\n return;\n }\n\n // Execute handler\n await handler(req, reply, done);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Fastify RPC middleware that routes incoming requests to registered server functions.\n * Wraps the generic createMiddleware with the RPC handler that reads the body, dispatches\n * to the matching function, and sends the JSON-serialized result.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Fastify preHandler hook function\n */\nexport const createRPCMiddleware: FastifyMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const rpcPrefix = options.rpcPrefix;\n const prefixRegex = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${rpcPrefix}/`;\n\n return createMiddleware({\n ...options,\n handler: async (\n req: FastifyRequest,\n reply: FastifyReply,\n _done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = new URL(req.url, \"http://localhost\");\n const url = reqUrl.pathname;\n\n // Defense-in-depth: validate prefix match via escaped regex even though\n // the outer createMiddleware gates on the same prefix already.\n // istanbul ignore if\n if (prefixRegex && !prefixRegex.test(url)) {\n return;\n }\n\n // Optional origin check: reject requests whose Origin header does not\n // match the configured origin. Requests without an Origin header\n // (curl, native clients) pass through unchecked.\n const origin = options.origin;\n const requestOrigin = req.headers.origin;\n if (origin && requestOrigin && requestOrigin !== origin) {\n reply.status(403).send({ error: REQUEST_FORBIDDEN });\n return;\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = serverFunctionsMap.get(functionName);\n\n if (!serverFunction) {\n reply.status(404).send({\n error: FUNCTION_NOT_FOUND,\n });\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (req.method.toUpperCase() !== method) {\n reply.status(405).send({ error: METHOD_NOT_ALLOWED });\n return;\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = reqUrl.searchParams.get(\"args\");\n // istanbul ignore else\n if (raw) args = JSON.parse(raw);\n } else {\n const body = await readBody(req);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const { data: dataResult, cancel } = serverFunction.handler(...args);\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n\n req.raw.on(\"close\", onClose);\n const data = await dataResult;\n req.raw.off(\"close\", onClose);\n\n // istanbul ignore else\n if (!reply.raw.headersSent) reply.status(200).send({ data });\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n reply.status(500).send(formatError(err, isProduction));\n }\n },\n });\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fastify.mjs","names":["fastifyRpcPlugin"],"sources":["../../src/options.ts","../../src/constants.ts","../../src/fastify/plugin.ts","../../src/fastify/helpers.ts","../../src/fastify/createMiddleware.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions = {\n contentType: \"application/json\",\n credentials: \"same-origin\",\n method: \"POST\",\n} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: \"__rpc\",\n adapter: \"express\",\n serverFiles: \"exact\",\n scanRoot: undefined,\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n} satisfies MiddlewareOptions;\n","export const OPERATION_ABORTED = \"Operation aborted\";\n\nexport const REQUEST_CANCELLED = \"Request was cancelled\";\n\nexport const FETCH_ERROR_PREFIX = \"Fetch error: \";\n\nexport const NO_SERVER_FUNCTION_FOUND = \"No server function found.\";\n\nexport const ERROR_LOADING_FILE = \"Error loading file:\";\n\nexport const FUNCTION_NOT_FOUND = \"Function not found\";\n\nexport const METHOD_NOT_ALLOWED = \"Method Not Allowed\";\n\nexport const REQUEST_FORBIDDEN = \"Forbidden\";\n\nexport const UNSUPPORTED_MEDIA_TYPE = \"Unsupported Media Type\";\n\nexport const INTERNAL_SERVER_ERROR = \"Internal Server Error\";\n\nexport const CLIENT_DISCONNECTED = \"client disconnected\";\n\n/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */\nexport const MIDDLEWARE_NAME_USED = (name: string) =>\n `The middleware name \"${name}\" is already used.`;\n\n/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */\nexport const INVALID_IDENTIFIER = (label: string, name: string) =>\n `Invalid ${label}: \"${name}\" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;\n\n/** Error message when a value fails the safe-path-segment validation. @param label - What kind of value was being validated. @param segment - The rejected value */\nexport const INVALID_PATH_SEGMENT = (label: string, segment: string) =>\n `Invalid ${label}: \"${segment}\" must match /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/`;\n\n/** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */\nexport const CONFIG_FILE_NOT_FOUND = (\n configFile: string,\n configFilePath: string,\n) =>\n ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;\n\nexport const NO_CONFIG_FOUND = ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n\n/** Error template for duplicate server function names across files. @param name - The duplicate registered name */\nexport const DUPLICATE_FUNCTION_NAME = (name: string) =>\n `Duplicate server function \"${name}\" detected. Each server function must have a unique name. Remove or rename the duplicate.`;\n","/** @module Fastify plugin. Exports the RPC plugin wrapped with `fastify-plugin` for lifecycle-compatible registration. */\n// inspired by https://github.com/royalswe/vike-fastify-boilerplate/blob/main/server/index.ts\nimport fp from \"fastify-plugin\";\nimport type { MiddlewareOptions } from \"../types.d.ts\";\nimport type {\n FastifyRPCPlugin,\n RegisteredFastifyRPCPlugin,\n} from \"./types.d.ts\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\nexport type { MiddlewareOptions };\n\nconst RpcPlugin: FastifyRPCPlugin = (\n fastify,\n initialOptions,\n done,\n) => {\n // Register RPC middleware as preHandler hook\n const rpcMiddleware = createRPCMiddleware(initialOptions);\n fastify.addHook(\"preHandler\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n rpcMiddleware(request, reply, resolve);\n });\n await next();\n });\n\n done();\n};\n\n// Export the plugin wrapped with fastify-plugin\nconst rpcPlugin = fp(RpcPlugin, {\n name: \"uni-rpc-fastify-plugin\",\n}) as RegisteredFastifyRPCPlugin;\n\nexport { rpcPlugin as default };\n","// @thednp/rpc/src/fastify/helpers.ts\nimport type { FastifyReply, FastifyRequest } from \"fastify\";\nimport type { ViteDevServer } from \"vite\";\nimport type { FastifyInstance } from \"fastify\";\nimport type { Buffer } from \"node:buffer\";\nimport type { BodyResult, JsonValue } from \"../types.d.ts\";\nimport fastifyRpcPlugin from \"./plugin.ts\";\n\n/**\n * Convenience function to load RPC config and register the RPC plugin to a Fastify instance.\n * Dynamically imports loadRPCConfig and registers the fastify-rpc plugin.\n * @param app - Fastify instance\n */\nexport async function attachRPC(app: FastifyInstance) {\n // The main plugin entry statically imports Vite, so loadRPCConfig is\n // imported lazily: function bundles that never call attachRPC (e.g.\n // serverless functions) keep Vite out of the bundle (or externalized).\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n await app.register(fastifyRpcPlugin, options);\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Fastify instance for development mode.\n * Uses an `onRequest` hook to delegate to Vite's connect-compatible middleware stack.\n * @param app - Fastify instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: FastifyInstance, vite: ViteDevServer) {\n app.addHook(\"onRequest\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n vite.middlewares(request.raw, reply.raw, resolve);\n });\n await next();\n });\n}\n\n/**\n * Reads and parses the HTTP request body from a Fastify request.\n * If Fastify's body parser already consumed the stream, uses the pre-parsed body from `req.body`.\n * @param req - Fastify request object\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n req: FastifyRequest,\n): Promise<BodyResult> => {\n return new Promise((resolve, reject) => {\n const contentType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const reqBody = req.body as JsonValue;\n\n if (reqBody !== undefined) {\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart\n ? (reqBody as Record<string, unknown>)\n : isJSON\n ? reqBody\n : isUrlEncoded\n ? (reqBody as Record<string, unknown>)\n : String(reqBody),\n } as BodyResult);\n return;\n }\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n req.raw[method](\"data\", onData);\n req.raw[method](\"end\", onEnd);\n req.raw[method](\"error\", onError);\n };\n\n let body = \"\";\n\n const onData = (chunk: Buffer) => {\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n try {\n const data = isMultipart\n ? { raw: body }\n : isUrlEncoded\n ? Object.fromEntries(new URLSearchParams(body))\n : JSON.parse(body);\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart ? (data as Record<string, unknown>) : data,\n } as BodyResult);\n } catch (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n const onError = (err: Error) => {\n toggleListeners();\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n\n/**\n * Issues an HTTP redirect on a Fastify reply using the native\n * `reply.redirect(location, status)` API (Fastify v5 signature: destination\n * URL first, status code optional). Defaults to `303 See Other` for\n * convention (Post/Redirect/Get).\n * @param reply - Fastify reply object\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n */\nexport const redirect = (\n reply: FastifyReply,\n location: string,\n status = 303,\n): void => {\n reply.redirect(location, status);\n};\n","// src/fastify/createMiddleware.ts\nimport type {\n FastifyReply,\n FastifyRequest,\n HookHandlerDoneFunction,\n} from \"fastify\";\nimport type {\n FastifyMiddlewareFn,\n FastifyMiddlewareOptions,\n} from \"./types.d.ts\";\nimport type { JsonValue } from \"@thednp/rpc\";\nimport type { RequestEvent } from \"@thednp/rpc/server\";\nimport {\n escapeRegExp,\n formatError,\n hasContentTypeMismatch,\n provideRequestContext,\n scanForServerFiles,\n serverFunctionsMap,\n} from \"@thednp/rpc/server\";\nimport { defaultMiddlewareOptions, defaultRPCOptions } from \"../options.ts\";\nimport {\n CLIENT_DISCONNECTED,\n FUNCTION_NOT_FOUND,\n METHOD_NOT_ALLOWED,\n MIDDLEWARE_NAME_USED,\n REQUEST_FORBIDDEN,\n UNSUPPORTED_MEDIA_TYPE,\n} from \"../constants.ts\";\nimport { readBody, redirect as fastifyRedirect } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Fastify preHandler hook with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Fastify preHandler hook function\n */\nexport const createMiddleware: FastifyMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n const middlewareName = options.name;\n const rpcPrefix = options.rpcPrefix;\n const path = options.path;\n const handler = options.handler;\n\n let name = middlewareName;\n if (!name) {\n name = \"viteRPCMiddleware-\" + middlewareCount;\n middlewareCount += 1;\n }\n if (middlewareStack.has(name)) {\n throw new Error(MIDDLEWARE_NAME_USED(name));\n }\n middlewareStack.add(name);\n\n // Hoist regex compilation out of per-request path. Escape the prefix to\n // prevent regex injection via metacharacters in the config string.\n const prefixRegex: RegExp | null = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : null;\n const pathMatcher: RegExp | null = path\n ? (typeof path === \"string\" ? new RegExp(path) : path)\n : null;\n\n const middlewareHandler = async (\n req: FastifyRequest,\n reply: FastifyReply,\n done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = new URL(req.url, \"http://localhost\");\n const url = reqUrl.pathname;\n\n // When serving from production server, scan for server files\n if (serverFunctionsMap.size === 0) {\n await scanForServerFiles();\n }\n\n // No need to continue when no handler provided\n if (!handler) {\n done();\n return;\n }\n\n // Path matching\n if (pathMatcher && !pathMatcher.test(url)) {\n done();\n return;\n }\n\n // rpcPrefix matching (boundary-safe via escaped regex)\n if (prefixRegex && !prefixRegex.test(url)) {\n done();\n return;\n }\n\n // Execute handler\n await handler(req, reply, done);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Fastify RPC middleware that routes incoming requests to registered server functions.\n * Wraps the generic createMiddleware with the RPC handler that reads the body, dispatches\n * to the matching function, and sends the JSON-serialized result.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Fastify preHandler hook function\n */\nexport const createRPCMiddleware: FastifyMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const rpcPrefix = options.rpcPrefix;\n const prefixRegex = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${rpcPrefix}/`;\n\n return createMiddleware({\n ...options,\n handler: async (\n req: FastifyRequest,\n reply: FastifyReply,\n _done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = new URL(req.url, \"http://localhost\");\n const url = reqUrl.pathname;\n\n // Defense-in-depth: validate prefix match via escaped regex even though\n // the outer createMiddleware gates on the same prefix already.\n // istanbul ignore if\n if (prefixRegex && !prefixRegex.test(url)) {\n return;\n }\n\n // Optional origin check: reject requests whose Origin header does not\n // match the configured origin. Requests without an Origin header\n // (curl, native clients) pass through unchecked.\n const origin = options.origin;\n const requestOrigin = req.headers.origin;\n if (origin && requestOrigin && requestOrigin !== origin) {\n reply.status(403).send({ error: REQUEST_FORBIDDEN });\n return;\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = serverFunctionsMap.get(functionName);\n\n if (!serverFunction) {\n reply.status(404).send({\n error: FUNCTION_NOT_FOUND,\n });\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (req.method.toUpperCase() !== method) {\n reply.status(405).send({ error: METHOD_NOT_ALLOWED });\n return;\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = reqUrl.searchParams.get(\"args\");\n // istanbul ignore else\n if (raw) args = JSON.parse(raw);\n } else {\n // Content-type enforcement: strict for json/text, lenient between forms.\n // Requests without a Content-Type header are exempt (curl/GET compat).\n // Checked BEFORE readBody so mismatched bodies are never buffered.\n if (\n hasContentTypeMismatch(\n serverFunction.options?.contentType ?? \"application/json\",\n req.headers[\"content-type\"],\n )\n ) {\n reply.status(415).send({ error: UNSUPPORTED_MEDIA_TYPE });\n return;\n }\n const body = await readBody(req);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const requestEvent: RequestEvent = {\n request: req,\n response: reply,\n nativeEvent: req,\n locals: {},\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n fastifyRedirect(reply, location, status);\n },\n };\n const { data: dataResult, cancel } = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n\n req.raw.on(\"close\", onClose);\n const data = await dataResult;\n req.raw.off(\"close\", onClose);\n\n // istanbul ignore else\n if (!requestEvent.redirected && !reply.raw.headersSent) {\n reply.status(200).send({ data });\n }\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n reply.status(500).send(formatError(err, isProduction));\n }\n },\n });\n};\n"],"mappings":"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"}
|
|
@@ -2,12 +2,14 @@ import fp from "fastify-plugin";
|
|
|
2
2
|
import { Connect } from "vite";
|
|
3
3
|
import "@thednp/rpc";
|
|
4
4
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
5
|
-
import { NextFunction, Request, Response } from "express";
|
|
5
|
+
import { NextFunction, Request, Response as Response$1 } from "express";
|
|
6
6
|
import { MiddlewareHandler } from "hono";
|
|
7
7
|
import "@hono/node-server";
|
|
8
|
+
import "hono/utils/http-status";
|
|
8
9
|
import "hono/factory";
|
|
9
10
|
import { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from "fastify";
|
|
10
11
|
import { Context, Next } from "koa";
|
|
12
|
+
import { Middleware } from "h3";
|
|
11
13
|
//#region src/express/types.d.ts
|
|
12
14
|
/**
|
|
13
15
|
* Express/Connect middleware handler signature used by the RPC middleware.
|
|
@@ -19,7 +21,7 @@ interface ExpressMiddlewareHooks {
|
|
|
19
21
|
* @param res - Node or Express response object
|
|
20
22
|
* @param next - Connect or Express next function
|
|
21
23
|
*/
|
|
22
|
-
handler: (req: IncomingMessage | Request, res: ServerResponse | Response, next: Connect.NextFunction | NextFunction) => Promise<void>;
|
|
24
|
+
handler: (req: IncomingMessage | Request, res: ServerResponse | Response$1, next: Connect.NextFunction | NextFunction) => Promise<void>;
|
|
23
25
|
}
|
|
24
26
|
//#endregion
|
|
25
27
|
//#region src/hono/types.d.ts
|
|
@@ -66,6 +68,19 @@ interface KoaMiddlewareHooks {
|
|
|
66
68
|
handler: (ctx: Context, next: Next) => Promise<void>;
|
|
67
69
|
}
|
|
68
70
|
//#endregion
|
|
71
|
+
//#region src/h3/types.d.ts
|
|
72
|
+
/**
|
|
73
|
+
* h3 middleware handler signature used by the RPC middleware.
|
|
74
|
+
*/
|
|
75
|
+
interface H3MiddlewareHooks {
|
|
76
|
+
/**
|
|
77
|
+
* The handler invoked for each matched request.
|
|
78
|
+
* @param event - h3 event object
|
|
79
|
+
* @param next - h3 next function
|
|
80
|
+
*/
|
|
81
|
+
handler: Middleware;
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
69
84
|
//#region src/types.d.ts
|
|
70
85
|
/**
|
|
71
86
|
* Maps each supported framework adapter to its middleware hooks (handler signatures).
|
|
@@ -80,6 +95,8 @@ interface FrameworkHooks {
|
|
|
80
95
|
fastify: FastifyMiddlewareHooks;
|
|
81
96
|
/** Koa middleware handler signature */
|
|
82
97
|
koa: KoaMiddlewareHooks;
|
|
98
|
+
/** h3 middleware handler signature */
|
|
99
|
+
h3: H3MiddlewareHooks;
|
|
83
100
|
}
|
|
84
101
|
/**
|
|
85
102
|
* ### @thednp/rpc
|
|
@@ -105,7 +122,7 @@ interface RpcPluginOptions {
|
|
|
105
122
|
* also compatible with the vite's Connect development server.
|
|
106
123
|
* @default express
|
|
107
124
|
*/
|
|
108
|
-
adapter: "express" | "hono" | "fastify" | "koa";
|
|
125
|
+
adapter: "express" | "hono" | "h3" | "fastify" | "koa";
|
|
109
126
|
/**
|
|
110
127
|
* Root directory from which the plugin scans for server files.
|
|
111
128
|
* Defaults to `<root>/src/api`. Use this in monorepos where server files
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../../../../src/express/types.d.ts","../../../../src/hono/types.d.ts","../../../../src/fastify/types.d.ts","../../../../src/koa/types.d.ts","../../../../src/types.d.ts","../../../../src/fastify/plugin.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../../../../src/express/types.d.ts","../../../../src/hono/types.d.ts","../../../../src/fastify/types.d.ts","../../../../src/koa/types.d.ts","../../../../src/h3/types.d.ts","../../../../src/types.d.ts","../../../../src/fastify/plugin.ts"],"mappings":";;;;;;;;;;;;;;;;UA+BiB;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiB,YACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;;UCzBU;;EAEf,SAAS;;;;;;;KCEC,uBAAuB;;;;KAKvB,6BAA6B,WAAW;;;;UA4BnC;;;;;;;EAOf,UACE,KAAK,gBACL,KAAK,cACL,MAAM,4BACH;;;;;;;UCzCU;;;;;;EAMf,UAAU,KAAK,SAAS,MAAM,SAAS;;;;;;;UClBxB;;;;;;EAMf,SAAS;;;;;;;;UCKM;;EAEf,SAAS;;EAET,MAAM;;EAEN,SAAS;;EAET,KAAK;;EAEL,IAAI;;;;;;;;UA8MW;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;UAGe,kBACf,UAAU;;;;EAKV;;;;;;;;;;;;EAaA,gBAAgB;;;;;;;;;;EAWhB;;;;;;;;EASA;;;;;;;;;;;;;;;;;;EAmBA,UAAU,eAAe;;;;cChTrB,WAEA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fp from "fastify-plugin";
|
|
2
|
-
import { scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
|
|
2
|
+
import { escapeRegExp, formatError, hasContentTypeMismatch, provideRequestContext, scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
|
|
3
3
|
//#region src/options.ts
|
|
4
4
|
const defaultRPCOptions = {
|
|
5
5
|
rpcPrefix: "__rpc",
|
|
@@ -13,66 +13,15 @@ const defaultMiddlewareOptions = {
|
|
|
13
13
|
origin: void 0
|
|
14
14
|
};
|
|
15
15
|
//#endregion
|
|
16
|
-
//#region src/tools.ts
|
|
17
|
-
/**
|
|
18
|
-
* Escapes special regex metacharacters in a string.
|
|
19
|
-
* Used to safely embed user-configurable values (like rpcPrefix) into regular expressions,
|
|
20
|
-
* preventing ReDoS and regex injection attacks.
|
|
21
|
-
* @param s - The raw string to escape
|
|
22
|
-
* @returns The escaped string safe for use in new RegExp()
|
|
23
|
-
*/
|
|
24
|
-
function escapeRegExp(s) {
|
|
25
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
26
|
-
}
|
|
27
|
-
//#endregion
|
|
28
16
|
//#region src/constants.ts
|
|
29
17
|
const FUNCTION_NOT_FOUND = "Function not found";
|
|
30
18
|
const METHOD_NOT_ALLOWED = "Method Not Allowed";
|
|
31
19
|
const REQUEST_FORBIDDEN = "Forbidden";
|
|
32
|
-
const
|
|
20
|
+
const UNSUPPORTED_MEDIA_TYPE = "Unsupported Media Type";
|
|
33
21
|
const CLIENT_DISCONNECTED = "client disconnected";
|
|
34
22
|
/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */
|
|
35
23
|
const MIDDLEWARE_NAME_USED = (name) => `The middleware name "${name}" is already used.`;
|
|
36
24
|
//#endregion
|
|
37
|
-
//#region src/server-helpers.ts
|
|
38
|
-
/**
|
|
39
|
-
* A typed error thrown from server functions.
|
|
40
|
-
* The middleware serializes the `message` and `code` in the response,
|
|
41
|
-
* allowing clients to recognise and handle specific error conditions.
|
|
42
|
-
*/
|
|
43
|
-
var RPCError = class extends Error {
|
|
44
|
-
/** Machine-readable error code (e.g. "VALIDATION_FAILED", "UNAUTHORIZED") */
|
|
45
|
-
code;
|
|
46
|
-
/** Optional diagnostic payload */
|
|
47
|
-
data;
|
|
48
|
-
constructor(message, code = "INTERNAL", data) {
|
|
49
|
-
super(message);
|
|
50
|
-
this.name = "RPCError";
|
|
51
|
-
this.code = code;
|
|
52
|
-
this.data = data;
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
/**
|
|
56
|
-
* Formats an error for the RPC middleware response.
|
|
57
|
-
* In development the full `RPCError` payload is included so developers
|
|
58
|
-
* can quickly identify issues. Unexpected exceptions never expose their
|
|
59
|
-
* message — only the generic "Internal Server Error" is sent, preventing
|
|
60
|
-
* information disclosure; server-side diagnostics are preserved via the
|
|
61
|
-
* middleware's `console.error` logging.
|
|
62
|
-
*/
|
|
63
|
-
const formatError = (err, isProduction) => {
|
|
64
|
-
if (isProduction) return { error: INTERNAL_SERVER_ERROR };
|
|
65
|
-
if (err instanceof RPCError) {
|
|
66
|
-
const payload = {
|
|
67
|
-
error: err.message || "Internal Server Error",
|
|
68
|
-
code: err.code
|
|
69
|
-
};
|
|
70
|
-
if (err.data !== void 0) payload.data = err.data;
|
|
71
|
-
return payload;
|
|
72
|
-
}
|
|
73
|
-
return { error: INTERNAL_SERVER_ERROR };
|
|
74
|
-
};
|
|
75
|
-
//#endregion
|
|
76
25
|
//#region src/fastify/helpers.ts
|
|
77
26
|
/**
|
|
78
27
|
* Reads and parses the HTTP request body from a Fastify request.
|
|
@@ -87,9 +36,10 @@ const readBody = (req) => {
|
|
|
87
36
|
if (reqBody !== void 0) {
|
|
88
37
|
const isJSON = contentType.includes("json");
|
|
89
38
|
const isMultipart = contentType.includes("multipart/form-data");
|
|
39
|
+
const isUrlEncoded = contentType.includes("urlencoded");
|
|
90
40
|
resolve({
|
|
91
|
-
contentType: isMultipart ? "multipart/form-data" : isJSON ? "application/json" : "text/plain",
|
|
92
|
-
data: isMultipart ? reqBody : isJSON ? reqBody : String(reqBody)
|
|
41
|
+
contentType: isMultipart ? "multipart/form-data" : isJSON ? "application/json" : isUrlEncoded ? "application/x-www-form-urlencoded" : "text/plain",
|
|
42
|
+
data: isMultipart ? reqBody : isJSON ? reqBody : isUrlEncoded ? reqBody : String(reqBody)
|
|
93
43
|
});
|
|
94
44
|
return;
|
|
95
45
|
}
|
|
@@ -107,10 +57,11 @@ const readBody = (req) => {
|
|
|
107
57
|
toggleListeners();
|
|
108
58
|
const isJSON = contentType.includes("json");
|
|
109
59
|
const isMultipart = contentType.includes("multipart/form-data");
|
|
60
|
+
const isUrlEncoded = contentType.includes("urlencoded");
|
|
110
61
|
try {
|
|
111
|
-
const data = isMultipart ? { raw: body } : JSON.parse(body);
|
|
62
|
+
const data = isMultipart ? { raw: body } : isUrlEncoded ? Object.fromEntries(new URLSearchParams(body)) : JSON.parse(body);
|
|
112
63
|
resolve({
|
|
113
|
-
contentType: isMultipart ? "multipart/form-data" : isJSON ? "application/json" : "text/plain",
|
|
64
|
+
contentType: isMultipart ? "multipart/form-data" : isJSON ? "application/json" : isUrlEncoded ? "application/x-www-form-urlencoded" : "text/plain",
|
|
114
65
|
data: isMultipart ? data : data
|
|
115
66
|
});
|
|
116
67
|
} catch (_e) {
|
|
@@ -127,6 +78,18 @@ const readBody = (req) => {
|
|
|
127
78
|
toggleListeners(true);
|
|
128
79
|
});
|
|
129
80
|
};
|
|
81
|
+
/**
|
|
82
|
+
* Issues an HTTP redirect on a Fastify reply using the native
|
|
83
|
+
* `reply.redirect(location, status)` API (Fastify v5 signature: destination
|
|
84
|
+
* URL first, status code optional). Defaults to `303 See Other` for
|
|
85
|
+
* convention (Post/Redirect/Get).
|
|
86
|
+
* @param reply - Fastify reply object
|
|
87
|
+
* @param location - The URL to redirect to
|
|
88
|
+
* @param status - HTTP status code, defaults to 303
|
|
89
|
+
*/
|
|
90
|
+
const redirect = (reply, location, status = 303) => {
|
|
91
|
+
reply.redirect(location, status);
|
|
92
|
+
};
|
|
130
93
|
//#endregion
|
|
131
94
|
//#region src/fastify/createMiddleware.ts
|
|
132
95
|
let middlewareCount = 0;
|
|
@@ -213,15 +176,32 @@ const createRPCMiddleware = (initialOptions = {}) => {
|
|
|
213
176
|
const raw = reqUrl.searchParams.get("args");
|
|
214
177
|
if (raw) args = JSON.parse(raw);
|
|
215
178
|
} else {
|
|
179
|
+
if (hasContentTypeMismatch(serverFunction.options?.contentType ?? "application/json", req.headers["content-type"])) {
|
|
180
|
+
reply.status(415).send({ error: UNSUPPORTED_MEDIA_TYPE });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
216
183
|
const body = await readBody(req);
|
|
217
184
|
args = Array.isArray(body.data) ? body.data : [body.data];
|
|
218
185
|
}
|
|
219
|
-
const
|
|
186
|
+
const requestEvent = {
|
|
187
|
+
request: req,
|
|
188
|
+
response: reply,
|
|
189
|
+
nativeEvent: req,
|
|
190
|
+
locals: {},
|
|
191
|
+
redirect: (location, status = 303) => {
|
|
192
|
+
requestEvent.redirected = {
|
|
193
|
+
location,
|
|
194
|
+
status
|
|
195
|
+
};
|
|
196
|
+
redirect(reply, location, status);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const { data: dataResult, cancel } = provideRequestContext(requestEvent, () => serverFunction.handler(...args));
|
|
220
200
|
const onClose = () => cancel(CLIENT_DISCONNECTED);
|
|
221
201
|
req.raw.on("close", onClose);
|
|
222
202
|
const data = await dataResult;
|
|
223
203
|
req.raw.off("close", onClose);
|
|
224
|
-
if (!reply.raw.headersSent) reply.status(200).send({ data });
|
|
204
|
+
if (!requestEvent.redirected && !reply.raw.headersSent) reply.status(200).send({ data });
|
|
225
205
|
} catch (err) {
|
|
226
206
|
console.error(String(err));
|
|
227
207
|
const isProduction = process.env.NODE_ENV === "production";
|