@thednp/rpc 0.2.1 → 0.3.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/dist/koa/koa.mjs CHANGED
@@ -1,15 +1,4 @@
1
- import { escapeRegExp, formatError, hasContentTypeMismatch, provideRequestContext, safeURL, scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
2
- //#region src/constants.ts
3
- const FUNCTION_NOT_FOUND = "Function not found";
4
- const METHOD_NOT_ALLOWED = "Method Not Allowed";
5
- const REQUEST_FORBIDDEN = "Forbidden";
6
- const UNSUPPORTED_MEDIA_TYPE = "Unsupported Media Type";
7
- const BAD_REQUEST = "Bad Request";
8
- const CLIENT_DISCONNECTED = "client disconnected";
9
- /** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */
10
- const MIDDLEWARE_NAME_USED = (name) => `The middleware name "${name}" is already used.`;
11
- //#endregion
12
- //#region src/options.ts
1
+ import { escapeRegExp, formatError, getGlobalPrefix, hasContentTypeMismatch, provideRequestContext, safeURL, scanForServerFiles } from "@thednp/rpc/server";
13
2
  const defaultRPCOptions = {
14
3
  rpcPrefix: "__rpc",
15
4
  adapter: "express",
@@ -22,6 +11,42 @@ const defaultMiddlewareOptions = {
22
11
  origin: void 0
23
12
  };
24
13
  //#endregion
14
+ //#region src/functionsMap.ts
15
+ /**
16
+ * Global symbol under which the shared `serverFunctionsByPrefix` map is stored
17
+ * on `globalThis`. Keeping it on a `Symbol.for` key makes it instance-stable
18
+ * across the bundled entry copies (`index.mjs`, `server.mjs`, `express.mjs`,
19
+ * ...) and dev-server hot reloads, exactly like the request-context storage in
20
+ * `context.ts`. Without this, `scanForServerFiles` (bundled into the plugin)
21
+ * would populate a map copy the adapter middleware could not read.
22
+ */
23
+ const functionsMapSymbol = Symbol.for("thednp.rpc.functionsMap");
24
+ /**
25
+ * Map of rpcPrefix -> Map of function names -> ServerFnEntry
26
+ * Enables multiple RPC instances with different prefixes to coexist
27
+ * without name collisions.
28
+ */
29
+ const serverFunctionsByPrefix = globalThis[functionsMapSymbol] ??= /* @__PURE__ */ new Map();
30
+ /**
31
+ * Gets or creates the function map for a specific prefix.
32
+ * @param prefix - The RPC prefix (e.g., "__rpc", "v1:rpc", "admin:rpc")
33
+ * @returns Map of function names to ServerFnEntry for that prefix
34
+ */
35
+ const getFunctionsForPrefix = (prefix) => {
36
+ if (!serverFunctionsByPrefix.has(prefix)) serverFunctionsByPrefix.set(prefix, /* @__PURE__ */ new Map());
37
+ return serverFunctionsByPrefix.get(prefix);
38
+ };
39
+ //#endregion
40
+ //#region src/constants.ts
41
+ const FUNCTION_NOT_FOUND = "Function not found";
42
+ const METHOD_NOT_ALLOWED = "Method Not Allowed";
43
+ const REQUEST_FORBIDDEN = "Forbidden";
44
+ const UNSUPPORTED_MEDIA_TYPE = "Unsupported Media Type";
45
+ const BAD_REQUEST = "Bad Request";
46
+ const CLIENT_DISCONNECTED = "client disconnected";
47
+ /** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */
48
+ const MIDDLEWARE_NAME_USED = (name) => `The middleware name "${name}" is already used.`;
49
+ //#endregion
25
50
  //#region src/koa/helpers.ts
26
51
  /**
27
52
  * Convenience function to load RPC config and attach the RPC middleware to a Koa app.
@@ -143,7 +168,7 @@ const middlewareStack = /* @__PURE__ */ new Set();
143
168
  const createMiddleware = (initialOptions = {}) => {
144
169
  const options = Object.assign({}, defaultMiddlewareOptions, initialOptions);
145
170
  const middlewareName = options.name;
146
- const rpcPrefix = options.rpcPrefix;
171
+ let rpcPrefix = options.rpcPrefix;
147
172
  const path = options.path;
148
173
  const handler = options.handler;
149
174
  let name = middlewareName;
@@ -157,10 +182,15 @@ const createMiddleware = (initialOptions = {}) => {
157
182
  const pathMatcher = path ? typeof path === "string" ? new RegExp(path) : path : null;
158
183
  const middlewareHandler = async (ctx, next) => {
159
184
  const url = safeURL(ctx.url).pathname;
160
- if (serverFunctionsMap.size === 0) await scanForServerFiles();
161
185
  if (!handler) return next();
162
186
  if (pathMatcher && !pathMatcher.test(url)) return next();
163
187
  if (prefixRegex && !prefixRegex.test(url)) return next();
188
+ rpcPrefix = rpcPrefix ?? "__rpc";
189
+ if (getFunctionsForPrefix(rpcPrefix).size === 0) await scanForServerFiles({
190
+ rpcPrefix,
191
+ serverFiles: options.serverFiles,
192
+ scanRoot: options.scanRoot
193
+ });
164
194
  await handler(ctx, next);
165
195
  };
166
196
  Object.defineProperty(middlewareHandler, "name", { value: name });
@@ -176,8 +206,9 @@ const createMiddleware = (initialOptions = {}) => {
176
206
  const createRPCMiddleware = (initialOptions = {}) => {
177
207
  const options = Object.assign({}, defaultMiddlewareOptions, { rpcPrefix: defaultRPCOptions.rpcPrefix }, initialOptions);
178
208
  const rpcPrefix = options.rpcPrefix;
209
+ const prefix = rpcPrefix || getGlobalPrefix() || "__rpc";
179
210
  const prefixRegex = rpcPrefix ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`) : null;
180
- const prefixReplace = `/${rpcPrefix}/`;
211
+ const prefixReplace = `/${prefix}/`;
181
212
  return createMiddleware({
182
213
  ...options,
183
214
  handler: async (ctx, _next) => {
@@ -192,7 +223,7 @@ const createRPCMiddleware = (initialOptions = {}) => {
192
223
  return;
193
224
  }
194
225
  const functionName = url.replace(prefixReplace, "");
195
- const serverFunction = serverFunctionsMap.get(functionName);
226
+ const serverFunction = getFunctionsForPrefix(prefix).get(functionName);
196
227
  if (!serverFunction) {
197
228
  ctx.status = 404;
198
229
  ctx.body = { error: FUNCTION_NOT_FOUND };
@@ -1 +1 @@
1
- {"version":3,"file":"koa.mjs","names":[],"sources":["../../src/constants.ts","../../src/options.ts","../../src/koa/helpers.ts","../../src/koa/createMiddleware.ts"],"sourcesContent":["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 BAD_REQUEST = \"Bad Request\";\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","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","// src/koa/helpers.ts\nimport type { Buffer } from \"node:buffer\";\nimport type { ViteDevServer } from \"vite\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\nimport type { BodyResult } from \"@thednp/rpc\";\nimport type { Koa } from \"./index.ts\";\nimport type { KoaContext } from \"./types.d.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to a Koa app.\n * Dynamically imports loadRPCConfig and registers the middleware.\n * @param app - Koa application instance\n */\nexport async function attachRPC(app: Koa) {\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\n const config = await loadRPCConfig();\n const { adapter: _adapter, ...options } = config;\n app.use(createRPCMiddleware(options));\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Koa app for development mode.\n * Bridges Koa's context-based middleware to Vite's Connect-compatible middleware stack\n * by forwarding Koa body, wrapping res.end, and delegating back to Koa on 404 or unhandled routes.\n * @param app - Koa application instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: Koa, vite: ViteDevServer): void {\n app.use(async (ctx: KoaContext, next) => {\n const req = ctx.req;\n const res = ctx.res;\n\n // Forward Koa body to req.body for Express/RPC middleware compatibility\n const requestBody = ctx.request?.body;\n if (requestBody !== undefined) {\n Object.assign(req, { body: requestBody });\n }\n\n const originalEnd = res.end.bind(res);\n let viteHandled = false;\n // @ts-ignore - Koa res.end type mismatch with Node's\n res.end = function (...args: unknown[]) {\n viteHandled = true;\n return originalEnd(args[0]);\n };\n\n await new Promise<void>((resolve) => {\n vite.middlewares(req, res, () => resolve(undefined));\n });\n\n // @ts-ignore - Koa res.end type mismatch with Node's\n res.end = originalEnd;\n\n if (!viteHandled || res.statusCode === 404) {\n await next();\n }\n });\n}\n\n/**\n * Reads and parses the HTTP request body from a Koa context.\n * If koa-body or another body parser already consumed the stream,\n * uses the pre-parsed body from `ctx.request.body`.\n * @param ctx - Koa context\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n ctx: KoaContext,\n): Promise<BodyResult> => {\n const contentType = ctx.request.headers[\"content-type\"]?.toLowerCase() || \"\";\n\n return new Promise((resolve, reject) => {\n // If an koa-body already consumed the stream\n // via app.use(koaBody()), use ctx.request.body directly\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n const reqBody = ctx.request.body;\n if (reqBody !== undefined) {\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 // OR read the body normally\n let body = \"\";\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n ctx.req[method](\"data\", onData);\n ctx.req[method](\"end\", onEnd);\n ctx.req[method](\"error\", onError);\n };\n\n const onData = (chunk: Buffer) => {\n // chunks.push(chunk);\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 (_er) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\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 Koa context. Koa's `ctx.redirect(location)`\n * defaults to `302` and sets the `Location` header; the status code must be\n * overridden *after* the call (setting it before is ignored, see\n * koajs/koa#857). Defaults to `303 See Other` for convention\n * (Post/Redirect/Get).\n * @param ctx - Koa context\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n */\nexport const redirect = (\n ctx: KoaContext,\n location: string,\n status = 303,\n): void => {\n ctx.redirect(location);\n ctx.status = status;\n};\n","// src/koa/createMiddleware.ts\nimport type { Context, Next } from \"koa\";\nimport type { KoaMiddlewareFn, KoaMiddlewareOptions } 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 safeURL,\n scanForServerFiles,\n serverFunctionsMap,\n} from \"@thednp/rpc/server\";\nimport {\n BAD_REQUEST,\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\";\n\nimport { defaultMiddlewareOptions, defaultRPCOptions } from \"../options.ts\";\nimport { readBody, redirect as koaRedirect } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Koa middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * Koa URL is normalized via `new URL()` to strip query strings before matching.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Koa middleware function\n */\nexport const createMiddleware: KoaMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as KoaMiddlewareOptions;\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 (ctx: Context, next: Next) => {\n const url = safeURL(ctx.url).pathname;\n\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 if (pathMatcher && !pathMatcher.test(url)) {\n return next();\n }\n\n if (prefixRegex && !prefixRegex.test(url)) {\n return next();\n }\n\n await handler(ctx, next);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Koa 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 sets the JSON-serialized result on ctx.body.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Koa middleware function\n */\nexport const createRPCMiddleware: KoaMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as KoaMiddlewareOptions;\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 (ctx: Context, _next: Next) => {\n const reqUrl = safeURL(ctx.url);\n const url = reqUrl.pathname;\n // const { rpcPrefix } = options;\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 next\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 = ctx.headers.origin;\n if (origin && requestOrigin && requestOrigin !== origin) {\n ctx.status = 403;\n ctx.body = { error: REQUEST_FORBIDDEN };\n return;\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = serverFunctionsMap.get(functionName);\n\n if (!serverFunction) {\n ctx.status = 404;\n ctx.body = { error: FUNCTION_NOT_FOUND };\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (ctx.method.toUpperCase() !== method) {\n ctx.status = 405;\n ctx.body = { error: METHOD_NOT_ALLOWED };\n return;\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = reqUrl.searchParams.get(\"args\");\n if (raw) {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n ctx.status = 400;\n ctx.body = { error: BAD_REQUEST };\n return;\n }\n args = parsed as JsonValue[];\n }\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 ctx.headers[\"content-type\"],\n )\n ) {\n ctx.status = 415;\n ctx.body = { error: UNSUPPORTED_MEDIA_TYPE };\n return;\n }\n const body = await readBody(ctx);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const requestEvent: RequestEvent = {\n request: ctx.req,\n response: ctx,\n nativeEvent: ctx,\n locals: ctx.state,\n functionName,\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n koaRedirect(ctx, location, status);\n },\n send: (status, body, headers) => {\n requestEvent.sent = { status, body, headers };\n if (headers) {\n for (const [name, value] of Object.entries(headers)) {\n ctx.set(name, value);\n }\n }\n ctx.status = status;\n ctx.body = body;\n },\n };\n const { data: resultData, cancel } = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n ctx.req.on(\"close\", onClose);\n const result = await resultData;\n ctx.req.off(\"close\", onClose);\n\n // Skip the JSON send when the server function issued a redirect or\n // short-circuited with `send`; the bound Koa adapter already set\n // ctx.status/ctx.body.\n // istanbul ignore else\n if (!requestEvent.redirected && !requestEvent.sent) {\n ctx.status = 200;\n ctx.body = { data: result };\n }\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n ctx.status = 500;\n ctx.body = formatError(err, isProduction);\n }\n },\n });\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"koa.mjs","names":[],"sources":["../../src/options.ts","../../src/functionsMap.ts","../../src/constants.ts","../../src/koa/helpers.ts","../../src/koa/createMiddleware.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions: ServerFunctionOptions = {\n contentType: \"application/json\",\n credentials: \"same-origin\",\n method: \"POST\",\n};\n\nexport const defaultPrefix = \"__rpc\";\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: defaultPrefix,\n adapter: \"express\",\n serverFiles: \"exact\",\n scanRoot: undefined,\n};\n\nexport const defaultMiddlewareOptions: MiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n};\n","import type { ServerFnEntry } from \"./types.d.ts\";\nimport { defaultPrefix } from \"./options.ts\";\n\n/**\n * Global symbol under which the shared `serverFunctionsByPrefix` map is stored\n * on `globalThis`. Keeping it on a `Symbol.for` key makes it instance-stable\n * across the bundled entry copies (`index.mjs`, `server.mjs`, `express.mjs`,\n * ...) and dev-server hot reloads, exactly like the request-context storage in\n * `context.ts`. Without this, `scanForServerFiles` (bundled into the plugin)\n * would populate a map copy the adapter middleware could not read.\n */\nconst functionsMapSymbol = Symbol.for(\"thednp.rpc.functionsMap\");\n\n/**\n * Map of rpcPrefix -> Map of function names -> ServerFnEntry\n * Enables multiple RPC instances with different prefixes to coexist\n * without name collisions.\n */\nexport const serverFunctionsByPrefix: Map<\n string,\n Map<string, ServerFnEntry>\n> = ((globalThis as Record<symbol, Map<string, Map<string, ServerFnEntry>>>)[\n functionsMapSymbol\n] ??= new Map());\n\n/**\n * Gets or creates the function map for a specific prefix.\n * @param prefix - The RPC prefix (e.g., \"__rpc\", \"v1:rpc\", \"admin:rpc\")\n * @returns Map of function names to ServerFnEntry for that prefix\n */\nexport const getFunctionsForPrefix = (\n prefix: string,\n): Map<string, ServerFnEntry> => {\n if (!serverFunctionsByPrefix.has(prefix)) {\n serverFunctionsByPrefix.set(prefix, new Map());\n }\n return serverFunctionsByPrefix.get(prefix)!;\n};\n\n/**\n * Backward compatibility: default map for the default prefix.\n * Legacy code can still use serverFunctionsMap.set(name, entry).\n */\nexport const serverFunctionsMap: Map<string, ServerFnEntry> = {\n get: (key: string) => getFunctionsForPrefix(defaultPrefix).get(key),\n set: (key: string, value: ServerFnEntry) =>\n getFunctionsForPrefix(defaultPrefix).set(key, value),\n has: (key: string) => getFunctionsForPrefix(defaultPrefix).has(key),\n delete: (key: string) => getFunctionsForPrefix(defaultPrefix).delete(key),\n clear: () => getFunctionsForPrefix(defaultPrefix).clear(),\n get size() {\n return getFunctionsForPrefix(defaultPrefix).size;\n },\n entries: () => getFunctionsForPrefix(defaultPrefix).entries(),\n keys: () => getFunctionsForPrefix(defaultPrefix).keys(),\n values: () => getFunctionsForPrefix(defaultPrefix).values(),\n forEach: (\n callback: (\n value: ServerFnEntry,\n key: string,\n map: Map<string, ServerFnEntry>,\n ) => void,\n ) => getFunctionsForPrefix(defaultPrefix).forEach(callback),\n [Symbol.iterator]: () =>\n getFunctionsForPrefix(defaultPrefix)[Symbol.iterator](),\n} as unknown as Map<string, ServerFnEntry>;\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 BAD_REQUEST = \"Bad Request\";\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","// src/koa/helpers.ts\nimport type { Buffer } from \"node:buffer\";\nimport type { ViteDevServer } from \"vite\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\nimport type { BodyResult } from \"@thednp/rpc\";\nimport type { Koa } from \"./index.ts\";\nimport type { KoaContext } from \"./types.d.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to a Koa app.\n * Dynamically imports loadRPCConfig and registers the middleware.\n * @param app - Koa application instance\n */\nexport async function attachRPC(app: Koa) {\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\n const config = await loadRPCConfig();\n const { adapter: _adapter, ...options } = config;\n app.use(createRPCMiddleware(options));\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Koa app for development mode.\n * Bridges Koa's context-based middleware to Vite's Connect-compatible middleware stack\n * by forwarding Koa body, wrapping res.end, and delegating back to Koa on 404 or unhandled routes.\n * @param app - Koa application instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: Koa, vite: ViteDevServer): void {\n app.use(async (ctx: KoaContext, next) => {\n const req = ctx.req;\n const res = ctx.res;\n\n // Forward Koa body to req.body for Express/RPC middleware compatibility\n const requestBody = ctx.request?.body;\n if (requestBody !== undefined) {\n Object.assign(req, { body: requestBody });\n }\n\n const originalEnd = res.end.bind(res);\n let viteHandled = false;\n // @ts-ignore - Koa res.end type mismatch with Node's\n res.end = function (...args: unknown[]) {\n viteHandled = true;\n return originalEnd(args[0]);\n };\n\n await new Promise<void>((resolve) => {\n vite.middlewares(req, res, () => resolve(undefined));\n });\n\n // @ts-ignore - Koa res.end type mismatch with Node's\n res.end = originalEnd;\n\n if (!viteHandled || res.statusCode === 404) {\n await next();\n }\n });\n}\n\n/**\n * Reads and parses the HTTP request body from a Koa context.\n * If koa-body or another body parser already consumed the stream,\n * uses the pre-parsed body from `ctx.request.body`.\n * @param ctx - Koa context\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n ctx: KoaContext,\n): Promise<BodyResult> => {\n const contentType = ctx.request.headers[\"content-type\"]?.toLowerCase() || \"\";\n\n return new Promise((resolve, reject) => {\n // If an koa-body already consumed the stream\n // via app.use(koaBody()), use ctx.request.body directly\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n const reqBody = ctx.request.body;\n if (reqBody !== undefined) {\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 // OR read the body normally\n let body = \"\";\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n ctx.req[method](\"data\", onData);\n ctx.req[method](\"end\", onEnd);\n ctx.req[method](\"error\", onError);\n };\n\n const onData = (chunk: Buffer) => {\n // chunks.push(chunk);\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 (_er) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\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 Koa context. Koa's `ctx.redirect(location)`\n * defaults to `302` and sets the `Location` header; the status code must be\n * overridden *after* the call (setting it before is ignored, see\n * koajs/koa#857). Defaults to `303 See Other` for convention\n * (Post/Redirect/Get).\n * @param ctx - Koa context\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n */\nexport const redirect = (\n ctx: KoaContext,\n location: string,\n status = 303,\n): void => {\n ctx.redirect(location);\n ctx.status = status;\n};\n","// src/koa/createMiddleware.ts\nimport type { Context, Next } from \"koa\";\nimport type { KoaMiddlewareFn, KoaMiddlewareOptions } from \"./types.d.ts\";\nimport type { JsonValue } from \"@thednp/rpc\";\nimport type { RequestEvent } from \"@thednp/rpc/server\";\nimport {\n escapeRegExp,\n formatError,\n getGlobalPrefix,\n hasContentTypeMismatch,\n provideRequestContext,\n safeURL,\n scanForServerFiles,\n} from \"@thednp/rpc/server\";\nimport { getFunctionsForPrefix } from \"../functionsMap.ts\";\nimport {\n BAD_REQUEST,\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\";\n\nimport {\n defaultMiddlewareOptions,\n defaultPrefix,\n defaultRPCOptions,\n} from \"../options.ts\";\nimport { readBody, redirect as koaRedirect } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Koa middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * Koa URL is normalized via `new URL()` to strip query strings before matching.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Koa middleware function\n */\nexport const createMiddleware: KoaMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as KoaMiddlewareOptions;\n\n const middlewareName = options.name;\n let 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 (ctx: Context, next: Next) => {\n const url = safeURL(ctx.url).pathname;\n\n // No need to continue when no handler provided\n if (!handler) {\n return next();\n }\n\n if (pathMatcher && !pathMatcher.test(url)) {\n return next();\n }\n\n if (prefixRegex && !prefixRegex.test(url)) {\n return next();\n }\n\n rpcPrefix = (rpcPrefix ?? defaultPrefix) as string;\n\n // When serving from production server, scan for server files\n if (getFunctionsForPrefix(rpcPrefix).size === 0) {\n await scanForServerFiles({\n rpcPrefix,\n serverFiles: (options as unknown as { serverFiles?: \"exact\" | \"glob\" })\n .serverFiles,\n scanRoot: (options as unknown as { scanRoot?: string }).scanRoot,\n } as never);\n }\n\n await handler(ctx, next);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Koa 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 sets the JSON-serialized result on ctx.body.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Koa middleware function\n */\nexport const createRPCMiddleware: KoaMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as KoaMiddlewareOptions;\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 prefix = rpcPrefix || getGlobalPrefix() || defaultPrefix;\n const prefixRegex = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${prefix}/`;\n\n return createMiddleware({\n ...options,\n handler: async (ctx: Context, _next: Next) => {\n const reqUrl = safeURL(ctx.url);\n const url = reqUrl.pathname;\n // const { rpcPrefix } = options;\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 next\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 = ctx.headers.origin;\n if (origin && requestOrigin && requestOrigin !== origin) {\n ctx.status = 403;\n ctx.body = { error: REQUEST_FORBIDDEN };\n return;\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = getFunctionsForPrefix(prefix).get(functionName);\n\n if (!serverFunction) {\n ctx.status = 404;\n ctx.body = { error: FUNCTION_NOT_FOUND };\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (ctx.method.toUpperCase() !== method) {\n ctx.status = 405;\n ctx.body = { error: METHOD_NOT_ALLOWED };\n return;\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = reqUrl.searchParams.get(\"args\");\n if (raw) {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n ctx.status = 400;\n ctx.body = { error: BAD_REQUEST };\n return;\n }\n args = parsed as JsonValue[];\n }\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 ctx.headers[\"content-type\"],\n )\n ) {\n ctx.status = 415;\n ctx.body = { error: UNSUPPORTED_MEDIA_TYPE };\n return;\n }\n const body = await readBody(ctx);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const requestEvent: RequestEvent = {\n request: ctx.req,\n response: ctx,\n nativeEvent: ctx,\n locals: ctx.state,\n functionName,\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n koaRedirect(ctx, location, status);\n },\n send: (status, body, headers) => {\n requestEvent.sent = { status, body, headers };\n if (headers) {\n for (const [name, value] of Object.entries(headers)) {\n ctx.set(name, value);\n }\n }\n ctx.status = status;\n ctx.body = body;\n },\n };\n const { data: resultData, cancel } = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n ctx.req.on(\"close\", onClose);\n const result = await resultData;\n ctx.req.off(\"close\", onClose);\n\n // Skip the JSON send when the server function issued a redirect or\n // short-circuited with `send`; the bound Koa adapter already set\n // ctx.status/ctx.body.\n // istanbul ignore else\n if (!requestEvent.redirected && !requestEvent.sent) {\n ctx.status = 200;\n ctx.body = { data: result };\n }\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n ctx.status = 500;\n ctx.body = formatError(err, isProduction);\n }\n },\n });\n};\n"],"mappings":"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"}
@@ -1,15 +1,95 @@
1
- import { ResolvedConfig, ViteDevServer } from "vite";
1
+ import { Connect, ResolvedConfig, ViteDevServer } from "vite";
2
2
  import "@thednp/rpc";
3
- import "express";
4
- import "hono";
3
+ import { IncomingMessage, ServerResponse } from "node:http";
4
+ import { NextFunction, Request, Response as Response$1 } from "express";
5
+ import { MiddlewareHandler } from "hono";
5
6
  import "@hono/node-server";
6
7
  import "hono/utils/http-status";
7
8
  import "hono/factory";
8
- import "fastify";
9
+ import { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from "fastify";
9
10
  import "fastify-plugin";
10
- import "koa";
11
- import "h3";
11
+ import { Context, Next } from "koa";
12
+ import { Middleware } from "h3";
13
+ //#region src/express/types.d.ts
14
+ /**
15
+ * Express/Connect middleware handler signature used by the RPC middleware.
16
+ */
17
+ interface ExpressMiddlewareHooks {
18
+ /**
19
+ * The handler invoked for each matched request.
20
+ * @param req - Node or Express request object
21
+ * @param res - Node or Express response object
22
+ * @param next - Connect or Express next function
23
+ */
24
+ handler: (req: IncomingMessage | Request, res: ServerResponse | Response$1, next: Connect.NextFunction | NextFunction) => Promise<void>;
25
+ }
26
+ //#endregion
27
+ //#region src/hono/types.d.ts
28
+ /**
29
+ * Hono middleware handler signature used by the RPC middleware.
30
+ */
31
+ interface HonoMiddlewareHooks {
32
+ /** Hono middleware handler */
33
+ handler: MiddlewareHandler;
34
+ }
35
+ //#endregion
36
+ //#region src/fastify/types.d.ts
37
+ /**
38
+ * Fastify middleware handler signature used by the RPC middleware.
39
+ */
40
+ interface FastifyMiddlewareHooks {
41
+ /**
42
+ * The handler invoked for each matched request.
43
+ * @param req - Fastify request object
44
+ * @param res - Fastify reply object
45
+ * @param done - Fastify hook completion callback
46
+ */
47
+ handler: (req: FastifyRequest, res: FastifyReply, done: HookHandlerDoneFunction) => Promise<void>;
48
+ }
49
+ //#endregion
50
+ //#region src/koa/types.d.ts
51
+ /**
52
+ * Koa middleware handler signature used by the RPC middleware.
53
+ */
54
+ interface KoaMiddlewareHooks {
55
+ /**
56
+ * The handler invoked for each matched request.
57
+ * @param ctx - Koa context object
58
+ * @param next - Koa next function
59
+ */
60
+ handler: (ctx: Context, next: Next) => Promise<void>;
61
+ }
62
+ //#endregion
63
+ //#region src/h3/types.d.ts
64
+ /**
65
+ * h3 middleware handler signature used by the RPC middleware.
66
+ */
67
+ interface H3MiddlewareHooks {
68
+ /**
69
+ * The handler invoked for each matched request.
70
+ * @param event - h3 event object
71
+ * @param next - h3 next function
72
+ */
73
+ handler: Middleware;
74
+ }
75
+ //#endregion
12
76
  //#region src/types.d.ts
77
+ /**
78
+ * Maps each supported framework adapter to its middleware hooks (handler signatures).
79
+ * Used to keep the middleware options type-safe per adapter.
80
+ */
81
+ interface FrameworkHooks {
82
+ /** Express/Connect middleware handler signature */
83
+ express: ExpressMiddlewareHooks;
84
+ /** Hono middleware handler signature */
85
+ hono: HonoMiddlewareHooks;
86
+ /** Fastify middleware handler signature */
87
+ fastify: FastifyMiddlewareHooks;
88
+ /** Koa middleware handler signature */
89
+ koa: KoaMiddlewareHooks;
90
+ /** h3 middleware handler signature */
91
+ h3: H3MiddlewareHooks;
92
+ }
13
93
  /**
14
94
  * Content types the RPC client modules send with each request.
15
95
  */
@@ -40,6 +120,11 @@ interface ServerFunctionOptions {
40
120
  * @default "POST"
41
121
  */
42
122
  method?: "GET" | "POST";
123
+ /**
124
+ * RPC endpoint prefix
125
+ * @default "__rpc"
126
+ */
127
+ rpcPrefix?: string;
43
128
  }
44
129
  // primitives and their compositions
45
130
  /**
@@ -98,12 +183,14 @@ interface RpcPluginOptionsInternal {
98
183
  /**
99
184
  * Partial Vite config used when scanning server files outside a running dev server.
100
185
  */
101
- type ScanConfig = Pick<ResolvedConfig, "base"> & {
186
+ interface ScanConfig extends Pick<ResolvedConfig, "base"> {
102
187
  root?: string;
103
188
  server?: Partial<ResolvedConfig["server"]>;
104
189
  serverFiles?: "exact" | "glob";
105
190
  scanRoot?: string;
106
- };
191
+ /** Default rpcPrefix to register scanned functions under when a function does not declare its own. Defaults to `__rpc` for backward compatibility. */
192
+ rpcPrefix?: string;
193
+ }
107
194
  /**
108
195
  * Entry in the server functions map: registered name, client handler,
109
196
  * optional per-function options, and the original export name.
@@ -158,8 +245,91 @@ interface RpcPluginOptions {
158
245
  */
159
246
  serverFiles?: "exact" | "glob";
160
247
  }
248
+ interface MiddlewareOptions<A extends RpcPluginOptions["adapter"] = "express"> {
249
+ /**
250
+ * Name for the middleware (used for identification in Express stack)
251
+ */
252
+ name?: string;
253
+ /**
254
+ * Path pattern to match for middleware execution.
255
+ * Accepts string or RegExp to filter requests based on URL path.
256
+ *
257
+ * @example
258
+ * // String path
259
+ * path: "/api/v1"
260
+ *
261
+ * // RegExp pattern
262
+ * path: /^\/api\/v[0-9]+/
263
+ */
264
+ path?: string | RegExp;
265
+ /**
266
+ * RPC prefix without leading slash (e.g. "__rpc")
267
+ * Leading slash will be added automatically by the middleware.
268
+ * This prefix defines the base path for all RPC endpoints.
269
+ * @default string
270
+ * @example
271
+ * // Results in endpoints like: /api/rpc/myFunction
272
+ * rpcPrefix: "api/rpc"
273
+ */
274
+ rpcPrefix?: string | false;
275
+ /**
276
+ * Allowed request origin (e.g. "https://example.com").
277
+ * When set, any request carrying an `Origin` header that does not match
278
+ * is rejected with a 403 Forbidden response. Requests without an `Origin`
279
+ * header (curl, native clients) pass through unchecked.
280
+ * When unset (default), no origin validation is performed.
281
+ */
282
+ origin?: string;
283
+ /**
284
+ * Server file matching mode. Use `"exact"` for `server.ts|js|mjs|mts`
285
+ * names, or `"glob"` to match `**\/*.server.{ts,js,mjs,mts}` inside the
286
+ * scan root. Only used for the lazy production scan when the middleware
287
+ * populates its prefix map on first request.
288
+ * @default "exact"
289
+ */
290
+ serverFiles?: "exact" | "glob";
291
+ /**
292
+ * Root directory for scanning server files. Defaults to `<root>/src/api`.
293
+ * Only used for the lazy production scan.
294
+ */
295
+ scanRoot?: string;
296
+ /**
297
+ * Async handler for request processing.
298
+ * Core middleware function that processes incoming requests.
299
+ *
300
+ * @param req - The incoming request object
301
+ * @param res - The server response object
302
+ * @param next - Function to pass control to the next middleware
303
+ *
304
+ * @example
305
+ * handler: async (req, res, next) => {
306
+ * // Process request
307
+ * const data = await processRequest(req);
308
+ *
309
+ * // Send response
310
+ * sendResponse(res, { data }, 200);
311
+ * }
312
+ */
313
+ handler?: FrameworkHooks[A]["handler"];
314
+ }
161
315
  //#endregion
162
316
  //#region src/functionsMap.d.ts
317
+ /**
318
+ * Map of rpcPrefix -> Map of function names -> ServerFnEntry
319
+ * Enables multiple RPC instances with different prefixes to coexist
320
+ * without name collisions.
321
+ */
322
+ declare const serverFunctionsByPrefix: Map<string, Map<string, ServerFnEntry>>;
323
+ /**
324
+ * Gets or creates the function map for a specific prefix.
325
+ * @param prefix - The RPC prefix (e.g., "__rpc", "v1:rpc", "admin:rpc")
326
+ * @returns Map of function names to ServerFnEntry for that prefix
327
+ */
328
+ declare const getFunctionsForPrefix: (prefix: string) => Map<string, ServerFnEntry>;
329
+ /**
330
+ * Backward compatibility: default map for the default prefix.
331
+ * Legacy code can still use serverFunctionsMap.set(name, entry).
332
+ */
163
333
  declare const serverFunctionsMap: Map<string, ServerFnEntry>;
164
334
  //#endregion
165
335
  //#region src/scanForServerFiles.d.ts
@@ -167,7 +337,7 @@ declare const serverFunctionsMap: Map<string, ServerFnEntry>;
167
337
  declare const scannedServerFiles: Set<string>;
168
338
  /**
169
339
  * Scans `src/api/` (or an explicit `scanRoot`) for server function files
170
- * and populates the global `serverFunctionsMap` with their exported functions.
340
+ * and populates the server functions map (scoped by rpcPrefix) with their exported functions.
171
341
  * Uses Vite's SSR module loading to resolve and execute each file.
172
342
  *
173
343
  * Supports two matching modes via `config.serverFiles`:
@@ -179,22 +349,49 @@ declare const scannedServerFiles: Set<string>;
179
349
  declare const scanForServerFiles: (initialCfg?: ScanConfig, devServer?: ViteDevServer) => Promise<void>;
180
350
  //#endregion
181
351
  //#region src/createFunction.d.ts
352
+ /**
353
+ * Extended options for createServerFunction, including rpcPrefix for multi-instance support.
354
+ */
355
+ interface CreateServerFunctionOptions extends Partial<ServerFunctionOptions> {
356
+ /**
357
+ * RPC prefix for this function. Enables multiple RPC instances with different prefixes.
358
+ * When using multi-prefix setup, functions with the same name but different prefixes
359
+ * can coexist without collision.
360
+ * @default "__rpc"
361
+ * @example
362
+ * // v1 API
363
+ * export const login = createServerFunction(
364
+ * "login",
365
+ * async (signal, email, password) => ({...}),
366
+ * { rpcPrefix: "v1:rpc" },
367
+ * );
368
+ *
369
+ * // v2 API - same function name, different prefix
370
+ * export const login = createServerFunction(
371
+ * "login",
372
+ * async (signal, credentials) => ({...}),
373
+ * { rpcPrefix: "v2:rpc" },
374
+ * );
375
+ */
376
+ rpcPrefix?: string;
377
+ }
182
378
  /**
183
379
  * Creates a server-side RPC function.
184
- * Registers the function in the server functions map and returns a client-compatible
185
- * wrapper that exposes `data` (Promise) and `cancel` (function) for request lifecycle control.
380
+ * Registers the function in the server functions map (scoped by rpcPrefix) and returns
381
+ * a client-compatible wrapper that exposes `data` (Promise) and `cancel` (function)
382
+ * for request lifecycle control.
186
383
  * @param name - Unique identifier used by the RPC router to dispatch requests
187
384
  * @param handler - The actual implementation receiving an AbortSignal followed by JSON-serializable arguments
188
- * @param fnOptions - Optional contentType and credentials settings
385
+ * @param fnOptions - Optional contentType, credentials, and rpcPrefix settings
189
386
  * @returns A client stub with `data` promise and `cancel` method, auto-registered in the server map
190
387
  */
191
- declare function createServerFunction<TArgs extends JsonArray = JsonArray, TResult extends JsonValue = JsonValue>(name: string, handler: ServerFunctionInit<TArgs, TResult>, fnOptions?: Partial<ServerFunctionOptions>): ClientFunction<TArgs, TResult>;
388
+ declare function createServerFunction<TArgs extends JsonArray = JsonArray, TResult extends JsonValue = JsonValue>(name: string, handler: ServerFunctionInit<TArgs, TResult>, fnOptions?: CreateServerFunctionOptions): ClientFunction<TArgs, TResult>;
192
389
  //#endregion
193
390
  //#region src/getClientModules.d.ts
194
391
  /**
195
392
  * Generates the complete client-side module bundle by iterating all registered server functions
196
- * and producing fetch-based stubs for each. The result is transformed by Vite (or Oxc) during
197
- * the dev server or production build.
393
+ * for a specific prefix and producing fetch-based stubs for each. The result is transformed by Vite
394
+ * (or Oxc) during the dev server or production build.
198
395
  * @param initialOptions - Plugin options containing rpcPrefix and optional adapter
199
396
  * @returns A string of JavaScript code with all client RPC modules and their import dependencies
200
397
  */
@@ -267,6 +464,9 @@ declare function escapeRegExp(s: string): string;
267
464
  * @returns A URL object; never throws
268
465
  */
269
466
  declare const safeURL: (rawUrl: string, base?: string) => URL;
467
+ /** Global rpcPrefix from the last loaded config / middleware — fallback for functions without explicit prefix. */
468
+ declare const getGlobalPrefix: () => string | undefined;
469
+ declare const setGlobalPrefix: (prefix: string | undefined) => void;
270
470
  //#endregion
271
471
  //#region src/context.d.ts
272
472
  /**
@@ -404,17 +604,10 @@ interface RequestMeta {
404
604
  declare const getRequestMeta: (event: RequestEvent) => RequestMeta;
405
605
  //#endregion
406
606
  //#region src/options.d.ts
407
- declare const defaultServerFnOptions: {
408
- contentType: "application/json";
409
- credentials: "same-origin";
410
- method: "POST";
411
- };
607
+ declare const defaultServerFnOptions: ServerFunctionOptions;
608
+ declare const defaultPrefix = "__rpc";
412
609
  declare const defaultRPCOptions: RpcPluginOptions;
413
- declare const defaultMiddlewareOptions: {
414
- rpcPrefix: undefined;
415
- path: undefined;
416
- origin: undefined;
417
- };
610
+ declare const defaultMiddlewareOptions: MiddlewareOptions;
418
611
  //#endregion
419
- export { RPCError, RequestEvent, RequestMeta, createServerFunction, defaultMiddlewareOptions, defaultRPCOptions, defaultServerFnOptions, escapeRegExp, formatError, getClientModules, getRequestContext, getRequestMeta, hasContentTypeMismatch, isFormContentType, provideRequestContext, redirect, safeURL, scanForServerFiles, scannedServerFiles, sendResponse, serverFunctionsMap, walkGlobFiles };
612
+ export { CreateServerFunctionOptions, RPCError, RequestEvent, RequestMeta, createServerFunction, defaultMiddlewareOptions, defaultPrefix, defaultRPCOptions, defaultServerFnOptions, escapeRegExp, formatError, getClientModules, getFunctionsForPrefix, getGlobalPrefix, getRequestContext, getRequestMeta, hasContentTypeMismatch, isFormContentType, provideRequestContext, redirect, safeURL, scanForServerFiles, scannedServerFiles, sendResponse, serverFunctionsByPrefix, serverFunctionsMap, setGlobalPrefix, walkGlobFiles };
420
613
  //# sourceMappingURL=server.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.mts","names":[],"sources":["../../src/types.d.ts","../../src/functionsMap.ts","../../src/scanForServerFiles.ts","../../src/createFunction.ts","../../src/getClientModules.ts","../../src/server-helpers.ts","../../src/context.ts","../../src/options.ts"],"mappings":";;;;;;;;;;;;;;;KAkEY;;;;KASA;;;;;UAkBK;;;;;EAKf,aAAa;;;;;EAKb,cAAc;;;;;;;EAOd;;;;;;KAOU;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,aAAa,WAAW;;;;KAIxB,YAAY,gBAAgB,YAAY;;;;;KAsCxC,mBACV,cAAc,WAAW,YAAY,WACrC,gBAAgB,YAAY,cACzB,QAAQ,gBAAgB,MAAM,UAAU,QAAQ;;;;;;KAOzC,eACV,cAAc,YAAY,WAC1B,gBAAgB,YAAY,iBACtB,MAAM;;EAEZ,MAAM,QAAQ;;EAEd,SAAS;;;;;;KAOC,4BAA4B;;EAEtC;;EAEA,UAAU;;;;;UAMK;;EAEf;;EAEA;;;;;KAMU,aAAa,KAAK;EAC5B;EACA,SAAS,QAAQ;EACjB;EACA;;;;;;UAOe;;EAEf;;EAEA,SAAS;;EAET,UAAU;;EAEV;;;;;;;;UASe;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;;;cChRW,oBAAoB,YAAY;;;;cCehC,oBAAoB;;;;;;;;;;;;cAepB,qBAAkB,aAChB,YAAU,YACX,kBACX;;;;;;;;;;;;iBCda,qBACd,cAAc,YAAY,WAC1B,gBAAgB,YAAY,WAE5B,cACA,SAAS,mBAAmB,OAAO,UACnC,YAAW,QAAQ,yBAClB,eAAe,OAAO;;;;;;;;;;cCgEZ,mBAAgB,gBACX;;;;;;;cChFL,gBAAa,gBAAwB;;;;;;cA4BrC,iBAAiB;;EAE5B;;EAEA,OAAO;EACP,YAAY,iBAAiB,eAAmB,OAAO;;;;;;;;;;cAgB5C,cAAW,cACV,0BAEX;;;;;;;cAqBU,oBAAiB;;;;;;;;;;;cAcjB,yBAAsB,UACvB,aAAW;;;;;;;;iBAqBP,aAAa;;;;;;;;;;;;;;cAmBhB,UAAO,gBAAkB,kBAAyB;;;;;;;;;;;;;;;;;;UC/G9C;;EAEf;;EAEA;;EAEA;;;;;;;EAOA,WAAW,kBAAkB;;;;;EAK7B;IAAe;IAAkB;;;;;;;;;;;EAUjC,OACE,gBACA,MAAM,WACN,UAAU;;;;;EAMZ;IAAS;IAAgB,MAAM;IAAW,UAAU;;;;;;EAKpD;;EAEA,QAAQ;GACP;;;;;;;;;cAiBU,wBAAyB,GAAC,MAC/B,cAAY,UACR,MACT;;;;;;cAOU,yBAAwB;;;;;;;;;cAgBxB,WAAQ,kBAAoB;;;;;;;;;;;;cAe5B,eAAY,gBACT,MACR,WAAS,UACL;;;;;;;UAWK;;EAEf;;EAEA;;EAEA;;EAEA,cAAc;;EAEd,SAAS;;EAET;;EAEA;;EAEA;;;;;;;;;;cAsCW,iBAAc,OAAW,iBAAe;;;cCnMxC;;;;;cAMA,mBAAmB;cAOnB"}
1
+ {"version":3,"file":"server.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/functionsMap.ts","../../src/scanForServerFiles.ts","../../src/createFunction.ts","../../src/getClientModules.ts","../../src/server-helpers.ts","../../src/context.ts","../../src/options.ts"],"mappings":"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"}