@thednp/rpc 0.0.1 → 0.0.5
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 +5 -5
- package/CLAUDE.md +1 -0
- package/README.md +195 -47
- package/dist/express/express.d.mts +110 -16
- package/dist/express/express.d.mts.map +1 -1
- package/dist/express/express.mjs +113 -20
- package/dist/express/express.mjs.map +1 -1
- package/dist/fastify/fastify.d.mts +83 -5
- package/dist/fastify/fastify.d.mts.map +1 -1
- package/dist/fastify/fastify.mjs +84 -18
- package/dist/fastify/fastify.mjs.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.d.mts +64 -9
- package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.mjs +73 -18
- package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
- package/dist/helpers/helpers.d.mts +66 -4
- package/dist/helpers/helpers.d.mts.map +1 -1
- package/dist/helpers/helpers.mjs +34 -7
- package/dist/helpers/helpers.mjs.map +1 -1
- package/dist/hono/hono.d.mts +75 -6
- package/dist/hono/hono.d.mts.map +1 -1
- package/dist/hono/hono.mjs +84 -24
- package/dist/hono/hono.mjs.map +1 -1
- package/dist/index.d.mts +210 -19
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +137 -31
- package/dist/index.mjs.map +1 -1
- package/dist/koa/koa.d.mts +52 -0
- package/dist/koa/koa.d.mts.map +1 -1
- package/dist/koa/koa.mjs +86 -16
- package/dist/koa/koa.mjs.map +1 -1
- package/dist/server/server.d.mts +111 -10
- package/dist/server/server.d.mts.map +1 -1
- package/dist/server/server.mjs +117 -17
- package/dist/server/server.mjs.map +1 -1
- package/package.json +48 -31
- package/wiki/adapters.md +0 -143
- package/wiki/best-practices.md +0 -201
- package/wiki/client-usage.md +0 -62
- package/wiki/configuration.md +0 -77
- package/wiki/getting-started.md +0 -76
- package/wiki/index.md +0 -26
- package/wiki/security.md +0 -54
- package/wiki/server-functions.md +0 -93
- package/wiki/setup.md +0 -77
package/dist/express/express.mjs
CHANGED
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
import { scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
|
|
2
2
|
//#region src/options.ts
|
|
3
3
|
const defaultRPCOptions = {
|
|
4
|
-
|
|
4
|
+
rpcPrefix: "__rpc",
|
|
5
5
|
adapter: "express"
|
|
6
6
|
};
|
|
7
7
|
const defaultMiddlewareOptions = {
|
|
8
|
-
|
|
9
|
-
path: void 0
|
|
8
|
+
rpcPrefix: void 0,
|
|
9
|
+
path: void 0,
|
|
10
|
+
origin: void 0
|
|
10
11
|
};
|
|
11
12
|
//#endregion
|
|
12
13
|
//#region src/express/helpers.ts
|
|
14
|
+
/**
|
|
15
|
+
* Convenience function to load RPC config and attach the RPC middleware to an Express app.
|
|
16
|
+
* Dynamically imports loadRPCConfig and creates the middleware with loaded options.
|
|
17
|
+
* @param app - Express application instance
|
|
18
|
+
*/
|
|
13
19
|
async function attachRPC(app) {
|
|
14
20
|
const { loadRPCConfig } = await import("@thednp/rpc");
|
|
15
21
|
const { adapter: _adapter, ...options } = await loadRPCConfig();
|
|
16
22
|
app.use(createRPCMiddleware(options));
|
|
17
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Attaches Vite's dev server middlewares to an Express app for development mode.
|
|
26
|
+
* @param app - Express application instance
|
|
27
|
+
* @param vite - Running Vite dev server
|
|
28
|
+
*/
|
|
18
29
|
function attachVite(app, vite) {
|
|
19
30
|
app.use(vite.middlewares);
|
|
20
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Reads and parses the HTTP request body from an Express or Node IncomingMessage.
|
|
34
|
+
* If a body parser middleware (e.g. express.json()) already consumed the stream,
|
|
35
|
+
* uses the pre-parsed body from `req.body`.
|
|
36
|
+
* @param req - Express or Node.js IncomingMessage
|
|
37
|
+
* @returns A promise resolving to the parsed body with its content type
|
|
38
|
+
*/
|
|
21
39
|
const readBody = (req) => {
|
|
22
40
|
return new Promise((resolve, reject) => {
|
|
23
41
|
if (hasPreParsedBody(req) && req.body !== void 0) {
|
|
@@ -61,15 +79,37 @@ const readBody = (req) => {
|
|
|
61
79
|
toggleListeners(true);
|
|
62
80
|
});
|
|
63
81
|
};
|
|
82
|
+
/**
|
|
83
|
+
* Type guard that checks whether a request is an Express Request (has `originalUrl`).
|
|
84
|
+
* @param req - A Node IncomingMessage or Express Request
|
|
85
|
+
* @returns True if the request is an Express Request
|
|
86
|
+
*/
|
|
64
87
|
const isExpressRequest = (req) => {
|
|
65
88
|
return "originalUrl" in req;
|
|
66
89
|
};
|
|
90
|
+
/**
|
|
91
|
+
* Type guard that checks whether a response is an Express Response (has `json` and `send` methods).
|
|
92
|
+
* @param res - A Node ServerResponse or Express Response
|
|
93
|
+
* @returns True if the response is an Express Response
|
|
94
|
+
*/
|
|
67
95
|
const isExpressResponse = (res) => {
|
|
68
96
|
return "json" in res && "send" in res;
|
|
69
97
|
};
|
|
98
|
+
/**
|
|
99
|
+
* Type guard that checks whether a request has a pre-parsed body (`body` property).
|
|
100
|
+
* Used to detect if a body-parser middleware already consumed the stream.
|
|
101
|
+
* @param req - A Node IncomingMessage or Express Request
|
|
102
|
+
* @returns True if the request has a body property
|
|
103
|
+
*/
|
|
70
104
|
const hasPreParsedBody = (req) => {
|
|
71
105
|
return "body" in req;
|
|
72
106
|
};
|
|
107
|
+
/**
|
|
108
|
+
* Extracts normalized request details from an Express or Node IncomingMessage.
|
|
109
|
+
* Parses the URL to extract pathname, search string, and search params.
|
|
110
|
+
* @param request - Express or Node.js request object
|
|
111
|
+
* @returns Normalized request details including URL, headers, and method
|
|
112
|
+
*/
|
|
73
113
|
const getRequestDetails = (request) => {
|
|
74
114
|
const rawUrl = isExpressRequest(request) ? request.originalUrl : request.url;
|
|
75
115
|
const url = new URL(rawUrl, "http://localhost");
|
|
@@ -81,6 +121,12 @@ const getRequestDetails = (request) => {
|
|
|
81
121
|
method: request.method
|
|
82
122
|
};
|
|
83
123
|
};
|
|
124
|
+
/**
|
|
125
|
+
* Wraps an Express or Node ServerResponse with a uniform API for setting headers,
|
|
126
|
+
* status codes, and sending JSON responses. Handles the Express vs raw Node API differences.
|
|
127
|
+
* @param response - Express or Node.js server response object
|
|
128
|
+
* @returns A ResponseDetails object with setHeader, setStatusCode, and sendResponse helpers
|
|
129
|
+
*/
|
|
84
130
|
const getResponseDetails = (response) => {
|
|
85
131
|
const isResponseSent = response.headersSent || response.writableEnded;
|
|
86
132
|
const setHeader = (name, value) => {
|
|
@@ -107,17 +153,40 @@ const getResponseDetails = (response) => {
|
|
|
107
153
|
};
|
|
108
154
|
//#endregion
|
|
109
155
|
//#region src/tools.ts
|
|
156
|
+
/**
|
|
157
|
+
* Escapes special regex metacharacters in a string.
|
|
158
|
+
* Used to safely embed user-configurable values (like rpcPrefix) into regular expressions,
|
|
159
|
+
* preventing ReDoS and regex injection attacks.
|
|
160
|
+
* @param s - The raw string to escape
|
|
161
|
+
* @returns The escaped string safe for use in new RegExp()
|
|
162
|
+
*/
|
|
110
163
|
function escapeRegExp(s) {
|
|
111
164
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
112
165
|
}
|
|
113
166
|
//#endregion
|
|
167
|
+
//#region src/constants.ts
|
|
168
|
+
const FUNCTION_NOT_FOUND = "Function not found";
|
|
169
|
+
const METHOD_NOT_ALLOWED = "Method Not Allowed";
|
|
170
|
+
const REQUEST_FORBIDDEN = "Forbidden";
|
|
171
|
+
const INTERNAL_SERVER_ERROR = "Internal Server Error";
|
|
172
|
+
const CLIENT_DISCONNECTED = "client disconnected";
|
|
173
|
+
/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */
|
|
174
|
+
const MIDDLEWARE_NAME_USED = (name) => `The middleware name "${name}" is already used.`;
|
|
175
|
+
//#endregion
|
|
114
176
|
//#region src/express/createMiddleware.ts
|
|
115
177
|
let middlewareCount = 0;
|
|
116
178
|
const middlewareStack = /* @__PURE__ */ new Set();
|
|
179
|
+
/**
|
|
180
|
+
* Creates an Express middleware with optional path and rpcPrefix filtering.
|
|
181
|
+
* Middleware names are deduplicated — reusing a name throws an error.
|
|
182
|
+
* Prefix and path regexes are compiled once at creation time (hoisted) for performance.
|
|
183
|
+
* @param initialOptions - Options for rpcPrefix, path matching, and the handler function
|
|
184
|
+
* @returns An Express middleware function
|
|
185
|
+
*/
|
|
117
186
|
const createMiddleware = (initialOptions = {}) => {
|
|
118
187
|
const options = Object.assign({}, defaultMiddlewareOptions, initialOptions);
|
|
119
188
|
const middlewareName = options.name;
|
|
120
|
-
const
|
|
189
|
+
const rpcPrefix = options.rpcPrefix;
|
|
121
190
|
const path = options.path;
|
|
122
191
|
const handler = options.handler;
|
|
123
192
|
let name = middlewareName;
|
|
@@ -125,50 +194,74 @@ const createMiddleware = (initialOptions = {}) => {
|
|
|
125
194
|
name = "viteRPCMiddleware-" + middlewareCount;
|
|
126
195
|
middlewareCount += 1;
|
|
127
196
|
}
|
|
128
|
-
if (middlewareStack.has(name)) throw new Error(
|
|
197
|
+
if (middlewareStack.has(name)) throw new Error(MIDDLEWARE_NAME_USED(name));
|
|
129
198
|
middlewareStack.add(name);
|
|
130
|
-
const prefixRegex =
|
|
199
|
+
const prefixRegex = rpcPrefix ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`) : null;
|
|
131
200
|
const pathMatcher = path ? typeof path === "string" ? new RegExp(path) : path : null;
|
|
132
|
-
const middlewareHandler = async (req,
|
|
201
|
+
const middlewareHandler = async (req, res, next) => {
|
|
133
202
|
const { url } = getRequestDetails(req);
|
|
134
203
|
if (serverFunctionsMap.size === 0) await scanForServerFiles();
|
|
135
204
|
if (!handler) return next?.();
|
|
136
205
|
if (pathMatcher && !pathMatcher.test(url)) return next?.();
|
|
137
206
|
if (prefixRegex && !prefixRegex.test(url)) return next?.();
|
|
138
|
-
await handler(req,
|
|
207
|
+
await handler(req, res, next);
|
|
139
208
|
};
|
|
140
209
|
Object.defineProperty(middlewareHandler, "name", { value: name });
|
|
141
210
|
return middlewareHandler;
|
|
142
211
|
};
|
|
212
|
+
/**
|
|
213
|
+
* Creates the Express RPC middleware that routes incoming requests to registered server functions.
|
|
214
|
+
* Reads the request body, dispatches to the matching function via serverFunctionsMap,
|
|
215
|
+
* and sends the JSON-serialized result. Handles client disconnection via abort signals.
|
|
216
|
+
* @param initialOptions - Options including rpcPrefix for URL routing
|
|
217
|
+
* @returns An Express middleware function
|
|
218
|
+
*/
|
|
143
219
|
const createRPCMiddleware = (initialOptions = {}) => {
|
|
144
|
-
const options = Object.assign({}, defaultMiddlewareOptions, {
|
|
145
|
-
const
|
|
146
|
-
const prefixRegex =
|
|
147
|
-
const prefixReplace = `/${
|
|
220
|
+
const options = Object.assign({}, defaultMiddlewareOptions, { rpcPrefix: defaultRPCOptions.rpcPrefix }, initialOptions);
|
|
221
|
+
const rpcPrefix = options.rpcPrefix;
|
|
222
|
+
const prefixRegex = rpcPrefix ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`) : null;
|
|
223
|
+
const prefixReplace = `/${rpcPrefix}/`;
|
|
148
224
|
return createMiddleware({
|
|
149
225
|
...options,
|
|
150
226
|
handler: async (req, res, _next) => {
|
|
151
|
-
const { url } = getRequestDetails(req);
|
|
227
|
+
const { url: path, searchParams } = getRequestDetails(req);
|
|
152
228
|
const { sendResponse } = getResponseDetails(res);
|
|
153
|
-
if (prefixRegex && !prefixRegex.test(
|
|
154
|
-
const
|
|
229
|
+
if (prefixRegex && !prefixRegex.test(path)) return;
|
|
230
|
+
const origin = options.origin;
|
|
231
|
+
const requestOrigin = req.headers.origin;
|
|
232
|
+
if (origin && requestOrigin && requestOrigin !== origin) {
|
|
233
|
+
sendResponse(403, { error: REQUEST_FORBIDDEN });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const functionName = path.replace(prefixReplace, "");
|
|
155
237
|
const serverFunction = serverFunctionsMap.get(functionName);
|
|
156
238
|
if (!serverFunction) {
|
|
157
|
-
sendResponse(404, { error:
|
|
239
|
+
sendResponse(404, { error: FUNCTION_NOT_FOUND });
|
|
158
240
|
return;
|
|
159
241
|
}
|
|
160
242
|
try {
|
|
161
|
-
const
|
|
162
|
-
|
|
243
|
+
const method = serverFunction.options?.method || "POST";
|
|
244
|
+
if (req.method?.toUpperCase() !== method) {
|
|
245
|
+
sendResponse(405, { error: METHOD_NOT_ALLOWED });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
let args = [];
|
|
249
|
+
if (method === "GET") {
|
|
250
|
+
const raw = searchParams.get("args");
|
|
251
|
+
if (raw) args = JSON.parse(raw);
|
|
252
|
+
} else {
|
|
253
|
+
const body = await readBody(req);
|
|
254
|
+
args = Array.isArray(body.data) ? body.data : [body.data];
|
|
255
|
+
}
|
|
163
256
|
const { data, cancel } = serverFunction.handler(...args);
|
|
164
|
-
const onClose = () => cancel(
|
|
257
|
+
const onClose = () => cancel(CLIENT_DISCONNECTED);
|
|
165
258
|
req.on("close", onClose);
|
|
166
259
|
const result = await data;
|
|
167
260
|
req.off("close", onClose);
|
|
168
261
|
if (!res.headersSent) sendResponse(200, { data: result });
|
|
169
262
|
} catch (err) {
|
|
170
263
|
console.error(String(err));
|
|
171
|
-
sendResponse(500, { error:
|
|
264
|
+
sendResponse(500, { error: INTERNAL_SERVER_ERROR });
|
|
172
265
|
}
|
|
173
266
|
}
|
|
174
267
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.mjs","names":[],"sources":["../../src/options.ts","../../src/express/helpers.ts","../../src/tools.ts","../../src/express/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} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPreffix: \"__rpc\",\n adapter: \"express\",\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPreffix: undefined,\n path: undefined,\n} satisfies MiddlewareOptions;\n","// src/express/helpers.ts\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type {\n Request as ExpressRequest,\n Response as ExpressResponse,\n} from \"express\";\nimport type { BodyResult, JsonValue } from \"@thednp/rpc\";\nimport { Buffer } from \"node:buffer\";\nimport type { ViteDevServer } from \"vite\";\nimport type { Express } from \"express\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\nexport async function attachRPC(app: Express) {\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n app.use(createRPCMiddleware(options));\n}\n\nexport function attachVite(app: Express, vite: ViteDevServer) {\n app.use(vite.middlewares);\n}\n\n// src/express/helpers.ts\nexport const readBody = (\n req: ExpressRequest | IncomingMessage,\n): Promise<BodyResult> => {\n return new Promise((resolve, reject) => {\n // If an Express body parser already consumed the stream\n // via app.use(express.json()), use req.body directly\n if (hasPreParsedBody(req) && req.body !== undefined) {\n // istanbul ignore next\n const contentType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const isJSON = contentType.includes(\"json\");\n resolve({\n contentType: isJSON ? \"application/json\" : \"text/plain\",\n data: isJSON ? req.body : String(req.body),\n } as BodyResult);\n return;\n }\n\n // Else we parse the body right away\n let body = \"\";\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n req[method](\"data\", onData);\n req[method](\"end\", onEnd);\n req[method](\"error\", onError);\n };\n\n const onData = (chunk: Buffer) => {\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const incomingType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const isJSON = incomingType.includes(\"json\");\n try {\n const data = JSON.parse(body);\n resolve({\n contentType: isJSON ? \"application/json\" : \"text/plain\",\n data,\n });\n } catch (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n\n const onError = (err: Error) => {\n toggleListeners();\n\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n\nexport const isExpressRequest = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"originalUrl\" in req;\n};\n\nexport const isExpressResponse = (\n res: ServerResponse | ExpressResponse,\n): res is ExpressResponse => {\n return \"json\" in res && \"send\" in res;\n};\n\nexport const hasPreParsedBody = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"body\" in req;\n};\n\nexport const getRequestDetails = (\n request: ExpressRequest | IncomingMessage,\n) => {\n const rawUrl = (\n isExpressRequest(request) ? request.originalUrl : request.url\n ) as string;\n const url = new URL(rawUrl, \"http://localhost\");\n\n return {\n url: url.pathname,\n search: url.search,\n searchParams: url.searchParams,\n headers: request.headers,\n method: request.method,\n };\n};\n\nexport const getResponseDetails = (\n response: ExpressResponse | ServerResponse,\n) => {\n const isResponseSent = response.headersSent || response.writableEnded;\n\n const setHeader = (name: string, value: string) => {\n if (isExpressResponse(response)) {\n response.header(name, value);\n } else {\n response.setHeader(name, value);\n }\n };\n\n const setStatusCode = (code: number) => {\n if (isExpressResponse(response)) {\n response.status(code);\n } else {\n response.statusCode = code;\n }\n };\n\n const sendResponse = (code: number, output: Record<string, JsonValue>) => {\n setStatusCode(code);\n setHeader(\"Content-Type\", \"application/json\");\n\n if (isExpressResponse(response)) {\n response.send(JSON.stringify(output));\n } else {\n response.end(JSON.stringify(output));\n }\n };\n\n return {\n isResponseSent,\n setHeader,\n statusCode: response.statusCode,\n setStatusCode,\n sendResponse,\n };\n};\n","export function escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n","// src/express/createMidleware.ts\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type {\n NextFunction,\n Request as ExpressRequest,\n Response as ExpressResponse,\n} from \"express\";\nimport type {\n ExpressMiddlewareFn,\n ExpressMiddlewareOptions,\n} from \"./types.d.ts\";\nimport type { Connect } from \"vite\";\n// import type { JsonArray, JsonValue } from \"@thednp/rpc\";\nimport { scanForServerFiles, serverFunctionsMap } from \"@thednp/rpc/server\";\nimport { defaultMiddlewareOptions, defaultRPCOptions } from \"../options.ts\";\nimport { getRequestDetails, getResponseDetails, readBody } from \"./helpers.ts\";\nimport { escapeRegExp } from \"../tools.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\nexport const createMiddleware: ExpressMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as ExpressMiddlewareOptions;\n const middlewareName = options.name;\n const rpcPreffix = options.rpcPreffix;\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(`The middleware name \"${name}\" is already used.`);\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 = rpcPreffix\n ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`)\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: IncomingMessage | ExpressRequest,\n _res: ServerResponse | ExpressResponse,\n next: Connect.NextFunction | NextFunction,\n ) => {\n const { url } = getRequestDetails(req);\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 return next?.();\n }\n\n // Path matching\n if (pathMatcher && !pathMatcher.test(url)) return next?.();\n\n // rpcPreffix matching (boundary-safe via escaped regex)\n if (prefixRegex && !prefixRegex.test(url)) {\n return next?.();\n }\n\n // Execute handler\n await handler(req, _res, next);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n// Create RPC middleware\nexport const createRPCMiddleware: ExpressMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPreffix: defaultRPCOptions.rpcPreffix },\n initialOptions,\n ) as ExpressMiddlewareOptions;\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 rpcPreffix = options.rpcPreffix;\n const prefixRegex = rpcPreffix\n ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${rpcPreffix}/`;\n\n return createMiddleware({\n ...options,\n handler: async (\n req: IncomingMessage | ExpressRequest,\n res: ServerResponse | ExpressResponse,\n _next: NextFunction | Connect.NextFunction,\n ) => {\n const { url } = getRequestDetails(req);\n const { sendResponse } = getResponseDetails(res);\n\n // Validate the url starts with the prefix via the escaped regex\n // istanbul ignore next\n if (prefixRegex && !prefixRegex.test(url)) {\n // falls through to next handler (never reached in practice; the outer\n // createMiddleware already gates on this, but kept for defense-in-depth)\n return;\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = serverFunctionsMap.get(functionName);\n\n if (!serverFunction) {\n sendResponse(404, { error: \"Function not found\" });\n return;\n }\n\n try {\n const body = await readBody(req);\n const args = Array.isArray(body.data) ? body.data : [body.data];\n const { data, cancel } = serverFunction.handler(...args);\n const onClose = () => cancel(\"client disconnected\");\n\n req.on(\"close\", onClose);\n const result = await data;\n req.off(\"close\", onClose);\n\n // istanbul ignore else\n if (!res.headersSent) sendResponse(200, { data: result });\n } catch (err) {\n console.error(String(err));\n sendResponse(500, { error: \"Internal Server Error\" });\n }\n },\n });\n};\n"],"mappings":";;AAUA,MAAa,oBAAsC;CACjD,YAAY;CACZ,SAAS;AACX;AAEA,MAAa,2BAA2B;CACtC,YAAY,KAAA;CACZ,MAAM,KAAA;AACR;;;ACNA,eAAsB,UAAU,KAAc;CAC5C,MAAM,EAAE,kBAAkB,MAAM,OAAO;CACvC,MAAM,EAAE,SAAS,UAAU,GAAG,YAAY,MAAM,cAAc;CAC9D,IAAI,IAAI,oBAAoB,OAAO,CAAC;AACtC;AAEA,SAAgB,WAAW,KAAc,MAAqB;CAC5D,IAAI,IAAI,KAAK,WAAW;AAC1B;AAGA,MAAa,YACX,QACwB;CACxB,OAAO,IAAI,SAAS,SAAS,WAAW;EAGtC,IAAI,iBAAiB,GAAG,KAAK,IAAI,SAAS,KAAA,GAAW;GAGnD,MAAM,UADc,IAAI,QAAQ,eAAe,EAAE,YAAY,KAAK,GAAA,CACvC,SAAS,MAAM;GAC1C,QAAQ;IACN,aAAa,SAAS,qBAAqB;IAC3C,MAAM,SAAS,IAAI,OAAO,OAAO,IAAI,IAAI;GAC3C,CAAe;GACf;EACF;EAGA,IAAI,OAAO;EAEX,MAAM,mBAAmB,QAAkB;GACzC,MAAM,SAAS,MAAM,OAAO;GAC5B,IAAI,OAAO,CAAC,QAAQ,MAAM;GAC1B,IAAI,OAAO,CAAC,OAAO,KAAK;GACxB,IAAI,OAAO,CAAC,SAAS,OAAO;EAC9B;EAEA,MAAM,UAAU,UAAkB;GAChC,QAAQ,MAAM,SAAS;EACzB;EAEA,MAAM,cAAc;GAClB,gBAAgB;GAEhB,MAAM,UADe,IAAI,QAAQ,eAAe,EAAE,YAAY,KAAK,GAAA,CACvC,SAAS,MAAM;GAC3C,IAAI;IACF,MAAM,OAAO,KAAK,MAAM,IAAI;IAC5B,QAAQ;KACN,aAAa,SAAS,qBAAqB;KAC3C;IACF,CAAC;GACH,SAAS,IAAI;IACX,QAAQ;KAAE,aAAa;KAAc,MAAM,OAAO,IAAI;IAAE,CAAC;GAC3D;EACF;EAEA,MAAM,WAAW,QAAe;GAC9B,gBAAgB;GAEhB,OAAO,GAAG;EACZ;EAEA,gBAAgB,IAAI;CACtB,CAAC;AACH;AAEA,MAAa,oBACX,QAC0B;CAC1B,OAAO,iBAAiB;AAC1B;AAEA,MAAa,qBACX,QAC2B;CAC3B,OAAO,UAAU,OAAO,UAAU;AACpC;AAEA,MAAa,oBACX,QAC0B;CAC1B,OAAO,UAAU;AACnB;AAEA,MAAa,qBACX,YACG;CACH,MAAM,SACJ,iBAAiB,OAAO,IAAI,QAAQ,cAAc,QAAQ;CAE5D,MAAM,MAAM,IAAI,IAAI,QAAQ,kBAAkB;CAE9C,OAAO;EACL,KAAK,IAAI;EACT,QAAQ,IAAI;EACZ,cAAc,IAAI;EAClB,SAAS,QAAQ;EACjB,QAAQ,QAAQ;CAClB;AACF;AAEA,MAAa,sBACX,aACG;CACH,MAAM,iBAAiB,SAAS,eAAe,SAAS;CAExD,MAAM,aAAa,MAAc,UAAkB;EACjD,IAAI,kBAAkB,QAAQ,GAC5B,SAAS,OAAO,MAAM,KAAK;OAE3B,SAAS,UAAU,MAAM,KAAK;CAElC;CAEA,MAAM,iBAAiB,SAAiB;EACtC,IAAI,kBAAkB,QAAQ,GAC5B,SAAS,OAAO,IAAI;OAEpB,SAAS,aAAa;CAE1B;CAEA,MAAM,gBAAgB,MAAc,WAAsC;EACxE,cAAc,IAAI;EAClB,UAAU,gBAAgB,kBAAkB;EAE5C,IAAI,kBAAkB,QAAQ,GAC5B,SAAS,KAAK,KAAK,UAAU,MAAM,CAAC;OAEpC,SAAS,IAAI,KAAK,UAAU,MAAM,CAAC;CAEvC;CAEA,OAAO;EACL;EACA;EACA,YAAY,SAAS;EACrB;EACA;CACF;AACF;;;ACzJA,SAAgB,aAAa,GAAmB;CAC9C,OAAO,EAAE,QAAQ,uBAAuB,MAAM;AAChD;;;ACgBA,IAAI,kBAAkB;AACtB,MAAM,kCAAkB,IAAI,IAAY;AAExC,MAAa,oBAAyC,iBAAiB,CAAC,MAAM;CAC5E,MAAM,UAAU,OAAO,OACrB,CAAC,GACD,0BACA,cACF;CACA,MAAM,iBAAiB,QAAQ;CAC/B,MAAM,aAAa,QAAQ;CAC3B,MAAM,OAAO,QAAQ;CACrB,MAAM,UAAU,QAAQ;CAExB,IAAI,OAAO;CACX,IAAI,CAAC,MAAM;EACT,OAAO,uBAAuB;EAC9B,mBAAmB;CACrB;CACA,IAAI,gBAAgB,IAAI,IAAI,GAC1B,MAAM,IAAI,MAAM,wBAAwB,KAAK,mBAAmB;CAElE,gBAAgB,IAAI,IAAI;CAIxB,MAAM,cAA6B,aAC/B,IAAI,OAAO,KAAK,aAAa,UAAU,EAAE,EAAE,IAC3C;CACJ,MAAM,cAA6B,OAC9B,OAAO,SAAS,WAAW,IAAI,OAAO,IAAI,IAAI,OAC/C;CAEJ,MAAM,oBAAoB,OACxB,KACA,MACA,SACG;EACH,MAAM,EAAE,QAAQ,kBAAkB,GAAG;EAGrC,IAAI,mBAAmB,SAAS,GAC9B,MAAM,mBAAmB;EAI3B,IAAI,CAAC,SACH,OAAO,OAAO;EAIhB,IAAI,eAAe,CAAC,YAAY,KAAK,GAAG,GAAG,OAAO,OAAO;EAGzD,IAAI,eAAe,CAAC,YAAY,KAAK,GAAG,GACtC,OAAO,OAAO;EAIhB,MAAM,QAAQ,KAAK,MAAM,IAAI;CAC/B;CAEA,OAAO,eAAe,mBAAmB,QAAQ,EAC/C,OAAO,KACT,CAAC;CAED,OAAO;AACT;AAGA,MAAa,uBACX,iBAAiB,CAAC,MACf;CACH,MAAM,UAAU,OAAO,OACrB,CAAC,GACD,0BACA,EAAE,YAAY,kBAAkB,WAAW,GAC3C,cACF;CAIA,MAAM,aAAa,QAAQ;CAC3B,MAAM,cAAc,aAChB,IAAI,OAAO,KAAK,aAAa,UAAU,EAAE,EAAE,IAC1C;CACL,MAAM,gBAAgB,IAAI,WAAW;CAErC,OAAO,iBAAiB;EACtB,GAAG;EACH,SAAS,OACP,KACA,KACA,UACG;GACH,MAAM,EAAE,QAAQ,kBAAkB,GAAG;GACrC,MAAM,EAAE,iBAAiB,mBAAmB,GAAG;GAI/C,IAAI,eAAe,CAAC,YAAY,KAAK,GAAG,GAGtC;GAGF,MAAM,eAAe,IAAI,QAAQ,eAAe,EAAE;GAClD,MAAM,iBAAiB,mBAAmB,IAAI,YAAY;GAE1D,IAAI,CAAC,gBAAgB;IACnB,aAAa,KAAK,EAAE,OAAO,qBAAqB,CAAC;IACjD;GACF;GAEA,IAAI;IACF,MAAM,OAAO,MAAM,SAAS,GAAG;IAC/B,MAAM,OAAO,MAAM,QAAQ,KAAK,IAAI,IAAI,KAAK,OAAO,CAAC,KAAK,IAAI;IAC9D,MAAM,EAAE,MAAM,WAAW,eAAe,QAAQ,GAAG,IAAI;IACvD,MAAM,gBAAgB,OAAO,qBAAqB;IAElD,IAAI,GAAG,SAAS,OAAO;IACvB,MAAM,SAAS,MAAM;IACrB,IAAI,IAAI,SAAS,OAAO;IAGxB,IAAI,CAAC,IAAI,aAAa,aAAa,KAAK,EAAE,MAAM,OAAO,CAAC;GAC1D,SAAS,KAAK;IACZ,QAAQ,MAAM,OAAO,GAAG,CAAC;IACzB,aAAa,KAAK,EAAE,OAAO,wBAAwB,CAAC;GACtD;EACF;CACF,CAAC;AACH"}
|
|
1
|
+
{"version":3,"file":"express.mjs","names":[],"sources":["../../src/options.ts","../../src/express/helpers.ts","../../src/tools.ts","../../src/constants.ts","../../src/express/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};\n\nexport const defaultMiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n} satisfies MiddlewareOptions;\n","// src/express/helpers.ts\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type {\n Request as ExpressRequest,\n Response as ExpressResponse,\n} from \"express\";\nimport type { BodyResult, JsonValue } from \"@thednp/rpc\";\nimport type { Buffer } from \"node:buffer\";\nimport type { ViteDevServer } from \"vite\";\nimport type { Express } from \"express\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\nimport type { RequestDetails, ResponseDetails } from \"./types.d.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to an Express app.\n * Dynamically imports loadRPCConfig and creates the middleware with loaded options.\n * @param app - Express application instance\n */\nexport async function attachRPC(app: Express) {\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n app.use(createRPCMiddleware(options));\n}\n\n/**\n * Attaches Vite's dev server middlewares to an Express app for development mode.\n * @param app - Express application instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: Express, vite: ViteDevServer) {\n app.use(vite.middlewares);\n}\n\n/**\n * Reads and parses the HTTP request body from an Express or Node IncomingMessage.\n * If a body parser middleware (e.g. express.json()) already consumed the stream,\n * uses the pre-parsed body from `req.body`.\n * @param req - Express or Node.js IncomingMessage\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n req: ExpressRequest | IncomingMessage,\n): Promise<BodyResult> => {\n return new Promise((resolve, reject) => {\n // If an Express body parser already consumed the stream\n // via app.use(express.json()), use req.body directly\n if (hasPreParsedBody(req) && req.body !== undefined) {\n // istanbul ignore next\n const contentType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const isJSON = contentType.includes(\"json\");\n resolve({\n contentType: isJSON ? \"application/json\" : \"text/plain\",\n data: isJSON ? req.body : String(req.body),\n } as BodyResult);\n return;\n }\n\n // Else we parse the body right away\n let body = \"\";\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n req[method](\"data\", onData);\n req[method](\"end\", onEnd);\n req[method](\"error\", onError);\n };\n\n const onData = (chunk: Buffer) => {\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const incomingType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const isJSON = incomingType.includes(\"json\");\n try {\n const data = JSON.parse(body);\n resolve({\n contentType: isJSON ? \"application/json\" : \"text/plain\",\n data,\n });\n } catch (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n\n const onError = (err: Error) => {\n toggleListeners();\n\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n\n/**\n * Type guard that checks whether a request is an Express Request (has `originalUrl`).\n * @param req - A Node IncomingMessage or Express Request\n * @returns True if the request is an Express Request\n */\nexport const isExpressRequest = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"originalUrl\" in req;\n};\n\n/**\n * Type guard that checks whether a response is an Express Response (has `json` and `send` methods).\n * @param res - A Node ServerResponse or Express Response\n * @returns True if the response is an Express Response\n */\nexport const isExpressResponse = (\n res: ServerResponse | ExpressResponse,\n): res is ExpressResponse => {\n return \"json\" in res && \"send\" in res;\n};\n\n/**\n * Type guard that checks whether a request has a pre-parsed body (`body` property).\n * Used to detect if a body-parser middleware already consumed the stream.\n * @param req - A Node IncomingMessage or Express Request\n * @returns True if the request has a body property\n */\nexport const hasPreParsedBody = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"body\" in req;\n};\n\n/**\n * Extracts normalized request details from an Express or Node IncomingMessage.\n * Parses the URL to extract pathname, search string, and search params.\n * @param request - Express or Node.js request object\n * @returns Normalized request details including URL, headers, and method\n */\nexport const getRequestDetails = (\n request: ExpressRequest | IncomingMessage,\n): RequestDetails => {\n const rawUrl = (\n isExpressRequest(request) ? request.originalUrl : request.url\n ) as string;\n const url = new URL(rawUrl, \"http://localhost\");\n\n return {\n url: url.pathname,\n search: url.search,\n searchParams: url.searchParams,\n headers: request.headers,\n method: request.method,\n };\n};\n\n/**\n * Wraps an Express or Node ServerResponse with a uniform API for setting headers,\n * status codes, and sending JSON responses. Handles the Express vs raw Node API differences.\n * @param response - Express or Node.js server response object\n * @returns A ResponseDetails object with setHeader, setStatusCode, and sendResponse helpers\n */\nexport const getResponseDetails = (\n response: ExpressResponse | ServerResponse,\n): ResponseDetails => {\n const isResponseSent = response.headersSent || response.writableEnded;\n\n const setHeader = (name: string, value: string) => {\n if (isExpressResponse(response)) {\n response.header(name, value);\n } else {\n response.setHeader(name, value);\n }\n };\n\n const setStatusCode = (code: number) => {\n if (isExpressResponse(response)) {\n response.status(code);\n } else {\n response.statusCode = code;\n }\n };\n\n const sendResponse = (code: number, output: Record<string, JsonValue>) => {\n setStatusCode(code);\n setHeader(\"Content-Type\", \"application/json\");\n\n if (isExpressResponse(response)) {\n response.send(JSON.stringify(output));\n } else {\n response.end(JSON.stringify(output));\n }\n };\n\n return {\n isResponseSent,\n setHeader,\n statusCode: response.statusCode,\n setStatusCode,\n sendResponse,\n };\n};\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","// src/express/createMidleware.ts\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type {\n NextFunction,\n Request as ExpressRequest,\n Response as ExpressResponse,\n} from \"express\";\nimport type {\n ExpressMiddlewareFn,\n ExpressMiddlewareOptions,\n} from \"./types.d.ts\";\nimport type { Connect } from \"vite\";\n// import type { JsonArray, JsonValue } from \"@thednp/rpc\";\nimport type { JsonValue } from \"../types.d.ts\";\nimport { scanForServerFiles, serverFunctionsMap } from \"@thednp/rpc/server\";\nimport { defaultMiddlewareOptions, defaultRPCOptions } from \"../options.ts\";\nimport { getRequestDetails, getResponseDetails, readBody } from \"./helpers.ts\";\nimport { escapeRegExp } from \"../tools.ts\";\nimport {\n CLIENT_DISCONNECTED,\n FUNCTION_NOT_FOUND,\n INTERNAL_SERVER_ERROR,\n METHOD_NOT_ALLOWED,\n MIDDLEWARE_NAME_USED,\n REQUEST_FORBIDDEN,\n} from \"../constants.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates an Express middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated — reusing a name throws an error.\n * Prefix and path regexes are compiled once at creation time (hoisted) for performance.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns An Express middleware function\n */\nexport const createMiddleware: ExpressMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as ExpressMiddlewareOptions;\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: IncomingMessage | ExpressRequest,\n res: ServerResponse | ExpressResponse,\n next: Connect.NextFunction | NextFunction,\n ) => {\n const { url } = getRequestDetails(req);\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 return next?.();\n }\n\n // Path matching\n if (pathMatcher && !pathMatcher.test(url)) return next?.();\n\n // rpcPrefix matching (boundary-safe via escaped regex)\n if (prefixRegex && !prefixRegex.test(url)) {\n return next?.();\n }\n\n // Execute handler\n await handler(req, res, next);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Express RPC middleware that routes incoming requests to registered server functions.\n * Reads the request body, dispatches to the matching function via serverFunctionsMap,\n * and sends the JSON-serialized result. Handles client disconnection via abort signals.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns An Express middleware function\n */\nexport const createRPCMiddleware: ExpressMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as ExpressMiddlewareOptions;\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: IncomingMessage | ExpressRequest,\n res: ServerResponse | ExpressResponse,\n _next: NextFunction | Connect.NextFunction,\n ) => {\n const { url: path, searchParams } = getRequestDetails(req);\n const { sendResponse } = getResponseDetails(res);\n\n // Validate the url starts with the prefix via the escaped regex\n // istanbul ignore next\n if (prefixRegex && !prefixRegex.test(path)) {\n // falls through to next handler (never reached in practice; the outer\n // createMiddleware already gates on this, but kept for defense-in-depth)\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 sendResponse(403, { error: REQUEST_FORBIDDEN });\n return;\n }\n\n const functionName = path.replace(prefixReplace, \"\");\n const serverFunction = serverFunctionsMap.get(functionName);\n\n if (!serverFunction) {\n sendResponse(404, { error: FUNCTION_NOT_FOUND });\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (req.method?.toUpperCase() !== method) {\n sendResponse(405, { error: METHOD_NOT_ALLOWED });\n return;\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = 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) ? body.data : [body.data];\n }\n const { data, cancel } = serverFunction.handler(...args);\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n\n req.on(\"close\", onClose);\n const result = await data;\n req.off(\"close\", onClose);\n\n // istanbul ignore else\n if (!res.headersSent) sendResponse(200, { data: result });\n } catch (err) {\n console.error(String(err));\n sendResponse(500, { error: INTERNAL_SERVER_ERROR });\n }\n },\n });\n};\n"],"mappings":"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"}
|
|
@@ -1,26 +1,75 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
1
2
|
import { FastifyInstance, FastifyReply, FastifyRequest, HookHandlerDoneFunction } from "fastify";
|
|
2
3
|
import { MiddlewareOptions, RpcPluginOptions } from "@thednp/rpc";
|
|
3
4
|
import { ViteDevServer } from "vite";
|
|
4
5
|
import "express";
|
|
5
6
|
import "hono";
|
|
6
7
|
import "@hono/node-server";
|
|
8
|
+
import "hono/factory";
|
|
7
9
|
import "koa";
|
|
8
10
|
//#region src/fastify/types.d.ts
|
|
11
|
+
/**
|
|
12
|
+
* Fastify RPC plugin signature: registers the middleware as a preHandler hook.
|
|
13
|
+
*/
|
|
14
|
+
type FastifyRPCPlugin = (fastify: FastifyInstance, initialOptions: Partial<MiddlewareOptions<"fastify">>, done: () => void) => void;
|
|
15
|
+
/**
|
|
16
|
+
* `fastify-plugin` function type, used to type the wrapped export.
|
|
17
|
+
*/
|
|
18
|
+
type FastifyPlugin = typeof fp;
|
|
19
|
+
/**
|
|
20
|
+
* Return type of `fastify-plugin` wrapping, matching the final plugin export.
|
|
21
|
+
*/
|
|
22
|
+
type RegisteredFastifyRPCPlugin = ReturnType<FastifyPlugin>;
|
|
23
|
+
/**
|
|
24
|
+
* Options accepted by the Fastify RPC plugin (`fp()`-wrapped registration).
|
|
25
|
+
*/
|
|
26
|
+
type RpcFastifyPluginOptions = MiddlewareOptions<"fastify"> & {
|
|
27
|
+
/** Whether this is an RPC plugin registration */
|
|
28
|
+
isRPC: boolean;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Fastify-specific middleware options, constrained to the `"fastify"` adapter.
|
|
32
|
+
*/
|
|
9
33
|
type FastifyMiddlewareOptions = MiddlewareOptions<"fastify">;
|
|
34
|
+
/**
|
|
35
|
+
* Fastify middleware factory: takes optional initial options and returns
|
|
36
|
+
* the Fastify-compatible handler.
|
|
37
|
+
*/
|
|
10
38
|
type FastifyMiddlewareFn = <A extends RpcPluginOptions["adapter"] = "fastify">(initialOptions?: Partial<FastifyMiddlewareOptions>) => FastifyMiddlewareHooks["handler"];
|
|
39
|
+
/**
|
|
40
|
+
* Fastify middleware handler signature used by the RPC middleware.
|
|
41
|
+
*/
|
|
11
42
|
interface FastifyMiddlewareHooks {
|
|
43
|
+
/**
|
|
44
|
+
* The handler invoked for each matched request.
|
|
45
|
+
* @param req - Fastify request object
|
|
46
|
+
* @param res - Fastify reply object
|
|
47
|
+
* @param done - Fastify hook completion callback
|
|
48
|
+
*/
|
|
12
49
|
handler: (req: FastifyRequest, res: FastifyReply, done: HookHandlerDoneFunction) => Promise<void>;
|
|
13
50
|
}
|
|
14
|
-
// Define the plugin function
|
|
15
|
-
type RpcFastifyPluginOptions = MiddlewareOptions<"fastify"> & {
|
|
16
|
-
isRPC: boolean;
|
|
17
|
-
};
|
|
18
51
|
//#endregion
|
|
19
52
|
//#region src/fastify/createMiddleware.d.ts
|
|
53
|
+
/**
|
|
54
|
+
* Creates a Fastify preHandler hook with optional path and rpcPrefix filtering.
|
|
55
|
+
* Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.
|
|
56
|
+
* @param initialOptions - Options for rpcPrefix, path matching, and the handler function
|
|
57
|
+
* @returns A Fastify preHandler hook function
|
|
58
|
+
*/
|
|
20
59
|
declare const createMiddleware: FastifyMiddlewareFn;
|
|
60
|
+
/**
|
|
61
|
+
* Creates the Fastify RPC middleware that routes incoming requests to registered server functions.
|
|
62
|
+
* Wraps the generic createMiddleware with the RPC handler that reads the body, dispatches
|
|
63
|
+
* to the matching function, and sends the JSON-serialized result.
|
|
64
|
+
* @param initialOptions - Options including rpcPrefix for URL routing
|
|
65
|
+
* @returns A Fastify preHandler hook function
|
|
66
|
+
*/
|
|
21
67
|
declare const createRPCMiddleware: FastifyMiddlewareFn;
|
|
22
68
|
//#endregion
|
|
23
69
|
//#region src/types.d.ts
|
|
70
|
+
/**
|
|
71
|
+
* Parsed request body result discriminated by content type.
|
|
72
|
+
*/
|
|
24
73
|
type BodyResult = {
|
|
25
74
|
contentType: "application/json";
|
|
26
75
|
data: JsonValue;
|
|
@@ -29,17 +78,46 @@ type BodyResult = {
|
|
|
29
78
|
data: string;
|
|
30
79
|
};
|
|
31
80
|
// primitives and their compositions
|
|
81
|
+
/**
|
|
82
|
+
* Primitive JSON values, including `undefined` for optional parameters.
|
|
83
|
+
*/
|
|
32
84
|
type JsonPrimitive = string | number | boolean | null | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* A JSON object whose values are JSON values or arrays.
|
|
87
|
+
*/
|
|
33
88
|
type JsonObject = {
|
|
34
89
|
[key: string]: JsonValue | JsonArray;
|
|
35
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* A JSON array of JSON values.
|
|
93
|
+
*/
|
|
36
94
|
type JsonArray = JsonValue[];
|
|
95
|
+
/**
|
|
96
|
+
* Any JSON-serializable value: primitive, array, or object.
|
|
97
|
+
*/
|
|
37
98
|
type JsonValue = JsonPrimitive | JsonArray | JsonObject;
|
|
38
99
|
//#endregion
|
|
39
100
|
//#region src/fastify/helpers.d.ts
|
|
101
|
+
/**
|
|
102
|
+
* Convenience function to load RPC config and register the RPC plugin to a Fastify instance.
|
|
103
|
+
* Dynamically imports loadRPCConfig and registers the fastify-rpc plugin.
|
|
104
|
+
* @param app - Fastify instance
|
|
105
|
+
*/
|
|
40
106
|
declare function attachRPC(app: FastifyInstance): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Attaches Vite's dev server middlewares to a Fastify instance for development mode.
|
|
109
|
+
* Uses an `onRequest` hook to delegate to Vite's connect-compatible middleware stack.
|
|
110
|
+
* @param app - Fastify instance
|
|
111
|
+
* @param vite - Running Vite dev server
|
|
112
|
+
*/
|
|
41
113
|
declare function attachVite(app: FastifyInstance, vite: ViteDevServer): void;
|
|
114
|
+
/**
|
|
115
|
+
* Reads and parses the HTTP request body from a Fastify request.
|
|
116
|
+
* If Fastify's body parser already consumed the stream, uses the pre-parsed body from `req.body`.
|
|
117
|
+
* @param req - Fastify request object
|
|
118
|
+
* @returns A promise resolving to the parsed body with its content type
|
|
119
|
+
*/
|
|
42
120
|
declare const readBody: (req: FastifyRequest) => Promise<BodyResult>;
|
|
43
121
|
//#endregion
|
|
44
|
-
export { type FastifyMiddlewareFn, type FastifyMiddlewareHooks, type FastifyMiddlewareOptions, type RpcFastifyPluginOptions, attachRPC, attachVite, createMiddleware, createRPCMiddleware, readBody };
|
|
122
|
+
export { type FastifyMiddlewareFn, type FastifyMiddlewareHooks, type FastifyMiddlewareOptions, type FastifyPlugin, type FastifyRPCPlugin, type RegisteredFastifyRPCPlugin, type RpcFastifyPluginOptions, attachRPC, attachVite, createMiddleware, createRPCMiddleware, readBody };
|
|
45
123
|
//# 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;;;;;;;;;;cChCM,kBAAkB;;;;;;;;cAgFlB,qBAAqB;;;;;;KC5CtB;EACN;EAAiC,MAAM;;EACvC;EAA2B;;;;;;KA8BrB;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,YAAY;;;;KAIZ,YAAY,gBAAgB,YAAY;;;;;;;;iBCpG9B,UAAU,KAAK,kBAAe;;;;;;;iBAYpC,WAAW,KAAK,iBAAiB,MAAM;;;;;;;cAgB1C,WAAQ,KACd,mBACJ,QAAQ"}
|
package/dist/fastify/fastify.mjs
CHANGED
|
@@ -2,20 +2,38 @@ import { scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
|
|
|
2
2
|
import fp from "fastify-plugin";
|
|
3
3
|
//#region src/options.ts
|
|
4
4
|
const defaultRPCOptions = {
|
|
5
|
-
|
|
5
|
+
rpcPrefix: "__rpc",
|
|
6
6
|
adapter: "express"
|
|
7
7
|
};
|
|
8
8
|
const defaultMiddlewareOptions = {
|
|
9
|
-
|
|
10
|
-
path: void 0
|
|
9
|
+
rpcPrefix: void 0,
|
|
10
|
+
path: void 0,
|
|
11
|
+
origin: void 0
|
|
11
12
|
};
|
|
12
13
|
//#endregion
|
|
13
14
|
//#region src/tools.ts
|
|
15
|
+
/**
|
|
16
|
+
* Escapes special regex metacharacters in a string.
|
|
17
|
+
* Used to safely embed user-configurable values (like rpcPrefix) into regular expressions,
|
|
18
|
+
* preventing ReDoS and regex injection attacks.
|
|
19
|
+
* @param s - The raw string to escape
|
|
20
|
+
* @returns The escaped string safe for use in new RegExp()
|
|
21
|
+
*/
|
|
14
22
|
function escapeRegExp(s) {
|
|
15
23
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16
24
|
}
|
|
17
25
|
//#endregion
|
|
26
|
+
//#region src/constants.ts
|
|
27
|
+
const FUNCTION_NOT_FOUND = "Function not found";
|
|
28
|
+
const METHOD_NOT_ALLOWED = "Method Not Allowed";
|
|
29
|
+
const REQUEST_FORBIDDEN = "Forbidden";
|
|
30
|
+
const INTERNAL_SERVER_ERROR = "Internal Server Error";
|
|
31
|
+
const CLIENT_DISCONNECTED = "client disconnected";
|
|
32
|
+
/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */
|
|
33
|
+
const MIDDLEWARE_NAME_USED = (name) => `The middleware name "${name}" is already used.`;
|
|
34
|
+
//#endregion
|
|
18
35
|
//#region src/fastify/plugin.ts
|
|
36
|
+
/** @module Fastify plugin. Exports the RPC plugin wrapped with `fastify-plugin` for lifecycle-compatible registration. */
|
|
19
37
|
const RpcPlugin = (fastify, initialOptions, done) => {
|
|
20
38
|
const rpcMiddleware = createRPCMiddleware(initialOptions);
|
|
21
39
|
fastify.addHook("preHandler", async (request, reply) => {
|
|
@@ -29,11 +47,22 @@ const RpcPlugin = (fastify, initialOptions, done) => {
|
|
|
29
47
|
const rpcPlugin = fp(RpcPlugin, { name: "uni-rpc-fastify-plugin" });
|
|
30
48
|
//#endregion
|
|
31
49
|
//#region src/fastify/helpers.ts
|
|
50
|
+
/**
|
|
51
|
+
* Convenience function to load RPC config and register the RPC plugin to a Fastify instance.
|
|
52
|
+
* Dynamically imports loadRPCConfig and registers the fastify-rpc plugin.
|
|
53
|
+
* @param app - Fastify instance
|
|
54
|
+
*/
|
|
32
55
|
async function attachRPC(app) {
|
|
33
56
|
const { loadRPCConfig } = await import("@thednp/rpc");
|
|
34
57
|
const { adapter: _adapter, ...options } = await loadRPCConfig();
|
|
35
58
|
await app.register(rpcPlugin, options);
|
|
36
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Attaches Vite's dev server middlewares to a Fastify instance for development mode.
|
|
62
|
+
* Uses an `onRequest` hook to delegate to Vite's connect-compatible middleware stack.
|
|
63
|
+
* @param app - Fastify instance
|
|
64
|
+
* @param vite - Running Vite dev server
|
|
65
|
+
*/
|
|
37
66
|
function attachVite(app, vite) {
|
|
38
67
|
app.addHook("onRequest", async (request, reply) => {
|
|
39
68
|
const next = () => new Promise((resolve) => {
|
|
@@ -42,6 +71,12 @@ function attachVite(app, vite) {
|
|
|
42
71
|
await next();
|
|
43
72
|
});
|
|
44
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Reads and parses the HTTP request body from a Fastify request.
|
|
76
|
+
* If Fastify's body parser already consumed the stream, uses the pre-parsed body from `req.body`.
|
|
77
|
+
* @param req - Fastify request object
|
|
78
|
+
* @returns A promise resolving to the parsed body with its content type
|
|
79
|
+
*/
|
|
45
80
|
const readBody = (req) => {
|
|
46
81
|
return new Promise((resolve, reject) => {
|
|
47
82
|
const contentType = req.headers["content-type"]?.toLowerCase() || "";
|
|
@@ -91,10 +126,16 @@ const readBody = (req) => {
|
|
|
91
126
|
//#region src/fastify/createMiddleware.ts
|
|
92
127
|
let middlewareCount = 0;
|
|
93
128
|
const middlewareStack = /* @__PURE__ */ new Set();
|
|
129
|
+
/**
|
|
130
|
+
* Creates a Fastify preHandler hook with optional path and rpcPrefix filtering.
|
|
131
|
+
* Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.
|
|
132
|
+
* @param initialOptions - Options for rpcPrefix, path matching, and the handler function
|
|
133
|
+
* @returns A Fastify preHandler hook function
|
|
134
|
+
*/
|
|
94
135
|
const createMiddleware = (initialOptions = {}) => {
|
|
95
136
|
const options = Object.assign({}, defaultMiddlewareOptions, initialOptions);
|
|
96
137
|
const middlewareName = options.name;
|
|
97
|
-
const
|
|
138
|
+
const rpcPrefix = options.rpcPrefix;
|
|
98
139
|
const path = options.path;
|
|
99
140
|
const handler = options.handler;
|
|
100
141
|
let name = middlewareName;
|
|
@@ -102,11 +143,11 @@ const createMiddleware = (initialOptions = {}) => {
|
|
|
102
143
|
name = "viteRPCMiddleware-" + middlewareCount;
|
|
103
144
|
middlewareCount += 1;
|
|
104
145
|
}
|
|
105
|
-
if (middlewareStack.has(name)) throw new Error(
|
|
146
|
+
if (middlewareStack.has(name)) throw new Error(MIDDLEWARE_NAME_USED(name));
|
|
106
147
|
middlewareStack.add(name);
|
|
107
|
-
const prefixRegex =
|
|
148
|
+
const prefixRegex = rpcPrefix ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`) : null;
|
|
108
149
|
const pathMatcher = path ? typeof path === "string" ? new RegExp(path) : path : null;
|
|
109
|
-
const middlewareHandler = async (req,
|
|
150
|
+
const middlewareHandler = async (req, reply, done) => {
|
|
110
151
|
const url = new URL(req.url, "http://localhost").pathname;
|
|
111
152
|
if (serverFunctionsMap.size === 0) await scanForServerFiles();
|
|
112
153
|
if (!handler) {
|
|
@@ -121,39 +162,64 @@ const createMiddleware = (initialOptions = {}) => {
|
|
|
121
162
|
done();
|
|
122
163
|
return;
|
|
123
164
|
}
|
|
124
|
-
await handler(req,
|
|
165
|
+
await handler(req, reply, done);
|
|
125
166
|
};
|
|
126
167
|
Object.defineProperty(middlewareHandler, "name", { value: name });
|
|
127
168
|
return middlewareHandler;
|
|
128
169
|
};
|
|
170
|
+
/**
|
|
171
|
+
* Creates the Fastify RPC middleware that routes incoming requests to registered server functions.
|
|
172
|
+
* Wraps the generic createMiddleware with the RPC handler that reads the body, dispatches
|
|
173
|
+
* to the matching function, and sends the JSON-serialized result.
|
|
174
|
+
* @param initialOptions - Options including rpcPrefix for URL routing
|
|
175
|
+
* @returns A Fastify preHandler hook function
|
|
176
|
+
*/
|
|
129
177
|
const createRPCMiddleware = (initialOptions = {}) => {
|
|
130
|
-
const options = Object.assign({}, defaultMiddlewareOptions, {
|
|
131
|
-
const
|
|
132
|
-
const prefixRegex =
|
|
133
|
-
const prefixReplace = `/${
|
|
178
|
+
const options = Object.assign({}, defaultMiddlewareOptions, { rpcPrefix: defaultRPCOptions.rpcPrefix }, initialOptions);
|
|
179
|
+
const rpcPrefix = options.rpcPrefix;
|
|
180
|
+
const prefixRegex = rpcPrefix ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`) : null;
|
|
181
|
+
const prefixReplace = `/${rpcPrefix}/`;
|
|
134
182
|
return createMiddleware({
|
|
135
183
|
...options,
|
|
136
184
|
handler: async (req, reply, _done) => {
|
|
137
|
-
const
|
|
185
|
+
const reqUrl = new URL(req.url, "http://localhost");
|
|
186
|
+
const url = reqUrl.pathname;
|
|
138
187
|
if (prefixRegex && !prefixRegex.test(url)) return;
|
|
188
|
+
const origin = options.origin;
|
|
189
|
+
const requestOrigin = req.headers.origin;
|
|
190
|
+
if (origin && requestOrigin && requestOrigin !== origin) {
|
|
191
|
+
reply.status(403).send({ error: REQUEST_FORBIDDEN });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
139
194
|
const functionName = url.replace(prefixReplace, "");
|
|
140
195
|
const serverFunction = serverFunctionsMap.get(functionName);
|
|
141
196
|
if (!serverFunction) {
|
|
142
|
-
reply.status(404).send({ error:
|
|
197
|
+
reply.status(404).send({ error: FUNCTION_NOT_FOUND });
|
|
143
198
|
return;
|
|
144
199
|
}
|
|
145
200
|
try {
|
|
146
|
-
const
|
|
147
|
-
|
|
201
|
+
const method = serverFunction.options?.method || "POST";
|
|
202
|
+
if (req.method.toUpperCase() !== method) {
|
|
203
|
+
reply.status(405).send({ error: METHOD_NOT_ALLOWED });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
let args = [];
|
|
207
|
+
if (method === "GET") {
|
|
208
|
+
const raw = reqUrl.searchParams.get("args");
|
|
209
|
+
if (raw) args = JSON.parse(raw);
|
|
210
|
+
} else {
|
|
211
|
+
const body = await readBody(req);
|
|
212
|
+
args = Array.isArray(body.data) ? body.data : [body.data];
|
|
213
|
+
}
|
|
148
214
|
const { data: dataResult, cancel } = serverFunction.handler(...args);
|
|
149
|
-
const onClose = () => cancel(
|
|
215
|
+
const onClose = () => cancel(CLIENT_DISCONNECTED);
|
|
150
216
|
req.raw.on("close", onClose);
|
|
151
217
|
const data = await dataResult;
|
|
152
218
|
req.raw.off("close", onClose);
|
|
153
219
|
if (!reply.raw.headersSent) reply.status(200).send({ data });
|
|
154
220
|
} catch (err) {
|
|
155
221
|
console.error(String(err));
|
|
156
|
-
reply.status(500).send({ error:
|
|
222
|
+
reply.status(500).send({ error: INTERNAL_SERVER_ERROR });
|
|
157
223
|
}
|
|
158
224
|
}
|
|
159
225
|
});
|