@thednp/rpc 0.3.2 → 0.3.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.3] - 2026-09-15
4
+
5
+ ### Features
6
+
7
+ - **Relaxed `JsonValue` constraint on `createServerFunction`** (`src/types.d.ts`, `src/createFunction.ts`): the generic `TResult` parameter of `ClientFunction`, `ServerFunction`, `ServerFunctionInit`, and `createServerFunction` no longer requires `extends JsonValue`. Only `getClientStub` and the internal `innerModule` retain the constraint for wire protocol safety. This eliminates double-casts and `LooseClientFunction` workarounds in wrapper libraries that define server functions with non-JSON return types
8
+ - **Hono header fallback in `getRequestMeta`** (`src/context.ts:218`): `getRequestMeta` now reads `req?.raw?.headers` as a fallback when `req?.headers` is undefined, matching Hono's internal request structure where native `Headers` live on `req.raw`. All other adapters pass headers via `req.headers` and are unaffected
9
+ - **`viteMiddleware` for Fastify** (`src/fastify/helpers.ts:40-68`): new `viteMiddleware(vite)` export returning an `onRequest` hook handler with `reply.hijack()` for streaming. Consistent with h3 and Hono's `viteMiddleware` — all three adapters now export a standalone middleware factory for custom Vite setups
10
+ - **`unwrapEnvelope<T>(json)` client helper** (`src/client-helpers.ts:44-55`, exported from `@thednp/rpc/helpers`): typed helper to unwrap `{ data }` wire protocol responses. For native HTTP clients or non-Vite toolchains that don't use the auto-generated fetch stubs. Re-throws `RPCError` on `{ error }` bodies
11
+ - **`silent` option on `loadRPCConfig` / `rpcPlugin`** (`src/config.ts:45`, `src/types.d.ts:239`, `src/index.ts:166`): new `silent?: boolean` suppresses the `NO_CONFIG_FOUND` warning. The plugin passes `devOptions.silent` through to `loadRPCConfig`, allowing wrapper plugins to define server functions directly without a config file
12
+
13
+ ### Chores
14
+
15
+ - Bump version to `0.3.3` (`package.json`, `deno.json`)
16
+
3
17
  ## [0.3.2] - 2026-08-25
4
18
 
5
19
  ### Features
@@ -49,6 +49,13 @@ interface RpcPluginOptions {
49
49
  * @default "exact"
50
50
  */
51
51
  serverFiles?: "exact" | "glob";
52
+ /**
53
+ * Suppress the "no RPC config found" warning when no config file is
54
+ * discovered. Useful when the plugin is wrapped by another tool that
55
+ * provides configuration externally (e.g. a meta-framework adapter).
56
+ * @default false
57
+ */
58
+ silent?: boolean;
52
59
  }
53
60
  //#endregion
54
61
  //#region src/config.d.ts
@@ -59,7 +66,6 @@ interface RpcPluginOptions {
59
66
  * @param uniConfig - System-wide RPC configuration overrides
60
67
  * @returns Complete RPC plugin options with defaults applied
61
68
  */
62
- declare const defineConfig: (c: Partial<RpcPluginOptions>) => RpcPluginOptions;
69
+ export declare const defineConfig: (c: Partial<RpcPluginOptions>) => RpcPluginOptions;
63
70
  //#endregion
64
- export { defineConfig };
65
71
  //# sourceMappingURL=config.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.mts","names":[],"sources":["../../src/types.d.ts","../../src/config.ts"],"mappings":";;;;;;;;;;;;;;;;;;UAsPiB;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;;;;;;;;;;cCvQW,eACX,GAAG,QAAQ,sBACR"}
1
+ {"version":3,"file":"config.d.mts","names":[],"sources":["../../src/types.d.ts","../../src/config.ts"],"mappings":";;;;;;;;;;;;;;;;;;UAsPiB;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;;;;;;;;;;qBC/QW,eACX,GAAG,QAAQ,sBACR"}
@@ -1 +1 @@
1
- {"version":3,"file":"config.mjs","names":[],"sources":["../../src/options.ts","../../src/config.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","/**\n * Vite-free configuration helpers.\n *\n * This module intentionally has zero runtime dependencies (not even on `vite`),\n * so `rpc.config.ts` files that import it stay safe to load in serverless\n * bundles where Vite is not installed. Importing the main plugin entry\n * (`@thednp/rpc`) instead would drag Vite into every server-side consumer.\n */\nimport type { RpcPluginOptions } from \"./types.d.ts\";\nimport { defaultRPCOptions } from \"./options.ts\";\n\n/**\n * Type-safe helper to create an RPC configuration object.\n * Merges the provided partial config over the built-in defaults,\n * skipping explicitly `undefined` values.\n * @param uniConfig - System-wide RPC configuration overrides\n * @returns Complete RPC plugin options with defaults applied\n */\nexport const defineConfig: (\n c: Partial<RpcPluginOptions>,\n) => RpcPluginOptions = (uniConfig: Partial<RpcPluginOptions>) => {\n const merged: RpcPluginOptions & Record<string, string> = {\n ...defaultRPCOptions,\n };\n for (const [key, value] of Object.entries(uniConfig)) {\n // istanbul ignore else\n if (value !== undefined) {\n merged[key] = value;\n }\n }\n return merged;\n};\n"],"mappings":"AAcA,MAAa,oBAAsC;CACjD,WAAW;CACX,SAAS;CACT,aAAa;CACb,UAAU,KAAA;AACZ;;;;;;;;;;ACDA,MAAa,gBAEY,cAAyC;CAChE,MAAM,SAAoD,EACxD,GAAG,kBACL;CACA,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS,GAEjD,IAAI,UAAU,KAAA,GACZ,OAAO,OAAO;CAGlB,OAAO;AACT"}
1
+ {"version":3,"file":"config.mjs","names":[],"sources":["../../src/options.ts","../../src/config.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","/**\n * Vite-free configuration helpers.\n *\n * This module intentionally has zero runtime dependencies (not even on `vite`),\n * so `rpc.config.ts` files that import it stay safe to load in serverless\n * bundles where Vite is not installed. Importing the main plugin entry\n * (`@thednp/rpc`) instead would drag Vite into every server-side consumer.\n */\nimport type { RpcPluginOptions } from \"./types.d.ts\";\nimport { defaultRPCOptions } from \"./options.ts\";\n\n/**\n * Type-safe helper to create an RPC configuration object.\n * Merges the provided partial config over the built-in defaults,\n * skipping explicitly `undefined` values.\n * @param uniConfig - System-wide RPC configuration overrides\n * @returns Complete RPC plugin options with defaults applied\n */\nexport const defineConfig: (\n c: Partial<RpcPluginOptions>,\n) => RpcPluginOptions = (uniConfig: Partial<RpcPluginOptions>) => {\n const merged: RpcPluginOptions & Record<string, unknown> = {\n ...defaultRPCOptions,\n };\n for (const [key, value] of Object.entries(uniConfig)) {\n // istanbul ignore else\n if (value !== undefined) {\n merged[key] = value;\n }\n }\n return merged;\n};\n"],"mappings":"AAcA,MAAa,oBAAsC;CACjD,WAAW;CACX,SAAS;CACT,aAAa;CACb,UAAU,KAAA;AACZ;;;;;;;;;;ACDA,MAAa,gBAEY,cAAyC;CAChE,MAAM,SAAqD,EACzD,GAAG,kBACL;CACA,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS,GAEjD,IAAI,UAAU,KAAA,GACZ,OAAO,OAAO;CAGlB,OAAO;AACT"}
@@ -64,7 +64,7 @@ type RequestDetails = {
64
64
  * @param initialOptions - Options for rpcPrefix, path matching, and the handler function
65
65
  * @returns An Express middleware function
66
66
  */
67
- declare const createMiddleware: ExpressMiddlewareFn;
67
+ export declare const createMiddleware: ExpressMiddlewareFn;
68
68
  /**
69
69
  * Creates the Express RPC middleware that routes incoming requests to registered server functions.
70
70
  * Reads the request body, dispatches to the matching function via getFunctionsForPrefix,
@@ -74,7 +74,7 @@ declare const createMiddleware: ExpressMiddlewareFn;
74
74
  * @param initialOptions - Options including rpcPrefix for URL routing and prefix-scoped function lookup
75
75
  * @returns An Express middleware function
76
76
  */
77
- declare const createRPCMiddleware: ExpressMiddlewareFn;
77
+ export declare const createRPCMiddleware: ExpressMiddlewareFn;
78
78
  //#endregion
79
79
  //#region src/express/helpers.d.ts
80
80
  /**
@@ -82,13 +82,13 @@ declare const createRPCMiddleware: ExpressMiddlewareFn;
82
82
  * Dynamically imports loadRPCConfig and creates the middleware with loaded options.
83
83
  * @param app - Express application instance
84
84
  */
85
- declare function attachRPC(app: Express$1): Promise<void>;
85
+ export declare function attachRPC(app: Express$1): Promise<void>;
86
86
  /**
87
87
  * Attaches Vite's dev server middlewares to an Express app for development mode.
88
88
  * @param app - Express application instance
89
89
  * @param vite - Running Vite dev server
90
90
  */
91
- declare function attachVite(app: Express$1, vite: ViteDevServer): void;
91
+ export declare function attachVite(app: Express$1, vite: ViteDevServer): void;
92
92
  /**
93
93
  * Reads and parses the HTTP request body from an Express or Node IncomingMessage.
94
94
  * If a body parser middleware (e.g. express.json()) already consumed the stream,
@@ -96,19 +96,19 @@ declare function attachVite(app: Express$1, vite: ViteDevServer): void;
96
96
  * @param req - Express or Node.js IncomingMessage
97
97
  * @returns A promise resolving to the parsed body with its content type
98
98
  */
99
- declare const readBody: (req: Request | IncomingMessage) => Promise<BodyResult>;
99
+ export declare const readBody: (req: Request | IncomingMessage) => Promise<BodyResult>;
100
100
  /**
101
101
  * Type guard that checks whether a request is an Express Request (has `originalUrl`).
102
102
  * @param req - A Node IncomingMessage or Express Request
103
103
  * @returns True if the request is an Express Request
104
104
  */
105
- declare const isExpressRequest: (req: IncomingMessage | Request) => req is Request;
105
+ export declare const isExpressRequest: (req: IncomingMessage | Request) => req is Request;
106
106
  /**
107
107
  * Type guard that checks whether a response is an Express Response (has `json` and `send` methods).
108
108
  * @param res - A Node ServerResponse or Express Response
109
109
  * @returns True if the response is an Express Response
110
110
  */
111
- declare const isExpressResponse: (res: ServerResponse | Response) => res is Response;
111
+ export declare const isExpressResponse: (res: ServerResponse | Response) => res is Response;
112
112
  /**
113
113
  * Issues an HTTP redirect on an Express or raw Node ServerResponse.
114
114
  * Uses Express's native `res.redirect(status, location)` when an Express
@@ -120,28 +120,28 @@ declare const isExpressResponse: (res: ServerResponse | Response) => res is Resp
120
120
  * @param location - The URL to redirect to
121
121
  * @param status - HTTP status code, defaults to 303
122
122
  */
123
- declare const redirect: (res: ServerResponse | Response, location: string, status?: number) => void;
123
+ export declare const redirect: (res: ServerResponse | Response, location: string, status?: number) => void;
124
124
  /**
125
125
  * Type guard that checks whether a request has a pre-parsed body (`body` property).
126
126
  * Used to detect if a body-parser middleware already consumed the stream.
127
127
  * @param req - A Node IncomingMessage or Express Request
128
128
  * @returns True if the request has a body property
129
129
  */
130
- declare const hasPreParsedBody: (req: IncomingMessage | Request) => req is Request;
130
+ export declare const hasPreParsedBody: (req: IncomingMessage | Request) => req is Request;
131
131
  /**
132
132
  * Extracts normalized request details from an Express or Node IncomingMessage.
133
133
  * Parses the URL to extract pathname, search string, and search params.
134
134
  * @param request - Express or Node.js request object
135
135
  * @returns Normalized request details including URL, headers, and method
136
136
  */
137
- declare const getRequestDetails: (request: Request | IncomingMessage) => RequestDetails;
137
+ export declare const getRequestDetails: (request: Request | IncomingMessage) => RequestDetails;
138
138
  /**
139
139
  * Wraps an Express or Node ServerResponse with a uniform API for setting headers,
140
140
  * status codes, and sending JSON responses. Handles the Express vs raw Node API differences.
141
141
  * @param response - Express or Node.js server response object
142
142
  * @returns A ResponseDetails object with setHeader, setStatusCode, and sendResponse helpers
143
143
  */
144
- declare const getResponseDetails: (response: Response | ServerResponse) => ResponseDetails;
144
+ export declare const getResponseDetails: (response: Response | ServerResponse) => ResponseDetails;
145
145
  //#endregion
146
- export { type Express, type ExpressMiddlewareFn, type ExpressMiddlewareHooks, type ExpressMiddlewareOptions, type ExpressNext, type ExpressRequest, type ExpressResponse, type RequestDetails, type ResponseDetails, attachRPC, attachVite, createMiddleware, createRPCMiddleware, getRequestDetails, getResponseDetails, hasPreParsedBody, isExpressRequest, isExpressResponse, readBody, redirect };
146
+ export type { Express, ExpressMiddlewareFn, ExpressMiddlewareHooks, ExpressMiddlewareOptions, ExpressNext, ExpressRequest, ExpressResponse, RequestDetails, ResponseDetails };
147
147
  //# sourceMappingURL=express.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"express.d.mts","names":[],"sources":["../../src/express/types.d.ts","../../src/express/createMiddleware.ts","../../src/express/helpers.ts"],"mappings":";;;;;;;;KAgBY,2BAA2B;;;;;KAM3B,uBACV,UAAU,yCAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiB,UACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;KAOK;;EAEV;;EAEA,YAAY,cAAc;;EAE1B;;EAEA,gBAAgB;;EAEhB,eAAe,cAAc,QAAQ;;;;;KAM3B;;EAEV;;EAEA;;EAEA,cAAc;;EAEd,SAAS;;EAET;;;;;;;;;;;cCrBW,kBAAkB;;;;;;;;;;cAiFlB,qBAAqB;;;;;;;;iBCpHZ,UAAU,KAAK,YAAO;;;;;;iBAc5B,WAAW,KAAK,WAAS,MAAM;;;;;;;;cAWlC,WAAQ,KACd,UAAiB,oBACrB,QAAQ;;;;;;cAqFE,mBAAgB,KACtB,kBAAkB,YACtB,OAAO;;;;;;cASG,oBAAiB,KACvB,iBAAiB,aACrB,OAAO;;;;;;;;;;;;cAeG,WAAQ,KACd,iBAAiB,UAAe,kBACrB;;;;;;;cAkBL,mBAAgB,KACtB,kBAAkB,YACtB,OAAO;;;;;;;cAUG,oBAAiB,SACnB,UAAiB,oBACzB;;;;;;;cAqBU,qBAAkB,UACnB,WAAkB,mBAC3B"}
1
+ {"version":3,"file":"express.d.mts","names":[],"sources":["../../src/express/types.d.ts","../../src/express/createMiddleware.ts","../../src/express/helpers.ts"],"mappings":";;;;;;;;KAgBY,2BAA2B;;;;;KAM3B,uBACV,UAAU,yCAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiB,UACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;KAOK;;EAEV;;EAEA,YAAY,cAAc;;EAE1B;;EAEA,gBAAgB;;EAEhB,eAAe,cAAc,QAAQ;;;;;KAM3B;;EAEV;;EAEA;;EAEA,cAAc;;EAEd,SAAS;;EAET;;;;;;;;;;;qBCrBW,kBAAkB;;;;;;;;;;qBAiFlB,qBAAqB;;;;;;;;wBCpHZ,UAAU,KAAK,YAAO;;;;;;wBAc5B,WAAW,KAAK,WAAS,MAAM;;;;;;;;qBAWlC,WAAQ,KACd,UAAiB,oBACrB,QAAQ;;;;;;qBAqFE,mBAAgB,KACtB,kBAAkB,YACtB,OAAO;;;;;;qBASG,oBAAiB,KACvB,iBAAiB,aACrB,OAAO;;;;;;;;;;;;qBAeG,WAAQ,KACd,iBAAiB,UAAe,kBACrB;;;;;;;qBAkBL,mBAAgB,KACtB,kBAAkB,YACtB,OAAO;;;;;;;qBAUG,oBAAiB,SACnB,UAAiB,oBACzB;;;;;;;qBAqBU,qBAAkB,UACnB,WAAkB,mBAC3B"}
@@ -1 +1 @@
1
- {"version":3,"file":"express.mjs","names":[],"sources":["../../src/options.ts","../../src/functionsMap.ts","../../src/constants.ts","../../src/server-helpers.ts","../../src/express/helpers.ts","../../src/express/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","/** @module Server-side utilities. Exports the `RPCError` class for typed server-side errors, `formatError` for middleware error responses, `isFormContentType` and `hasContentTypeMismatch` for content-type validation, and `walkGlobFiles` for recursively discovering `*.server.*` files. Never import this module in client code — it is server-only. */\nimport type { ContentType, JsonObject, JsonValue } from \"./types.d.ts\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { INTERNAL_SERVER_ERROR } from \"./constants.ts\";\n\nconst GLOB_REGEX = /^.+\\.server\\.(ts|js|mjs|mts)$/;\n\n/**\n * Recursively walks `dir` and collects absolute paths to files whose\n * basename matches the `*.server.{ts,js,mjs,mts}` glob pattern.\n */\nexport const walkGlobFiles = async (dir: string): Promise<string[]> => {\n const results: string[] = [];\n const stack = [dir];\n while (stack.length) {\n const current = stack.pop()!;\n let entries;\n try {\n entries = await readdir(current, { withFileTypes: true });\n } catch (_e) {\n continue;\n }\n for (const entry of entries) {\n const fullPath = join(current, entry.name);\n if (entry.isFile() && GLOB_REGEX.test(entry.name)) {\n results.push(fullPath);\n } else if (entry.isDirectory()) {\n stack.push(fullPath);\n }\n }\n }\n return results;\n};\n\n/**\n * A typed error thrown from server functions.\n * The middleware serializes the `message` and `code` in the response,\n * allowing clients to recognise and handle specific error conditions.\n */\nexport class RPCError extends Error {\n /** Machine-readable error code (e.g. \"VALIDATION_FAILED\", \"UNAUTHORIZED\") */\n code: string;\n /** Optional diagnostic payload */\n data?: JsonValue;\n constructor(message: string, code = \"INTERNAL\", data?: JsonValue) {\n super(message);\n this.name = \"RPCError\";\n this.code = code;\n this.data = data;\n }\n}\n\n/**\n * Formats an error for the RPC middleware response.\n * In development the full `RPCError` payload is included so developers\n * can quickly identify issues. Unexpected exceptions never expose their\n * message — only the generic \"Internal Server Error\" is sent, preventing\n * information disclosure; server-side diagnostics are preserved via the\n * middleware's `console.error` logging.\n */\nexport const formatError = (\n err: unknown,\n isProduction: boolean,\n): JsonObject => {\n if (isProduction) {\n return { error: INTERNAL_SERVER_ERROR };\n }\n if (err instanceof RPCError) {\n const payload: JsonObject = {\n error: err.message || INTERNAL_SERVER_ERROR,\n code: err.code,\n };\n if (err.data !== undefined) payload.data = err.data;\n return payload;\n }\n return { error: INTERNAL_SERVER_ERROR };\n};\n\n/**\n * Checks whether a content type maps to a form encoding\n * (`multipart/form-data` or `application/x-www-form-urlencoded`).\n * Form-declared functions accept either encoding so native browser\n * submissions (urlencoded) keep working without JavaScript.\n */\nexport const isFormContentType = (contentType: string): boolean =>\n contentType === \"multipart/form-data\" ||\n contentType === \"application/x-www-form-urlencoded\";\n\n/**\n * Detects whether an incoming request's `Content-Type` header conflicts\n * with the function's declared content type. JSON and text functions are\n * enforced strictly (exact match wins), while form functions accept both\n * form encodings because the nojs fallback submits urlencoded forms to\n * multipart-declared endpoints. Requests without a `Content-Type` header\n * (curl, GET, legacy clients) are exempt from enforcement.\n * @param declared - The declared `contentType` from the server function options\n * @param rawHeader - The raw `Content-Type` request header, if present\n */\nexport const hasContentTypeMismatch = (\n declared: ContentType,\n rawHeader: string | undefined,\n): boolean => {\n // No Content-Type header → exempt (url bar, GET, curl compatibility)\n if (!rawHeader) return false;\n // Strip parameters (charset, boundary) before comparison\n const incomingType = rawHeader.trim().toLowerCase().split(\";\")[0].trim();\n if (isFormContentType(declared)) {\n // Forms: reject only non-form encodings (lenient between the two)\n return !isFormContentType(incomingType);\n }\n return incomingType !== declared;\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\nconst SAFE_URL_BASE = \"http://localhost\";\n\n/**\n * Parses a raw request URL against a fixed base without ever throwing.\n * Malformed request-targets (e.g. `/\\`, `//`, `/\\/`) make the WHATWG URL\n * parser throw `TypeError: Invalid URL`; the adapters call this while\n * building the per-request URL **before** their dispatch `try` block, so an\n * unhandled rejection there crashes raw `node:http` hosts (and Express 4).\n * On failure we fall back to the base root: the resulting pathname never\n * matches the RPC prefix, so the request is treated as non-RPC and falls\n * through to `next()` / 404 instead of crashing the process.\n * @param rawUrl - Raw request URL (path + optional query string)\n * @param base - Optional base URL, defaults to a fixed localhost origin\n * @returns A URL object; never throws\n */\nexport const safeURL = (rawUrl: string, base = SAFE_URL_BASE): URL => {\n try {\n return new URL(rawUrl, base);\n } catch {\n return new URL(\"/\", base);\n }\n};\n\nconst globalPrefixSymbol = Symbol.for(\"thednp.rpc.globalPrefix\");\n\n/** Global rpcPrefix from the last loaded config / middleware — fallback for functions without explicit prefix. */\nexport const getGlobalPrefix = (): string | undefined =>\n (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ];\n\nexport const setGlobalPrefix = (prefix: string | undefined): void => {\n if (prefix) {\n (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ] = prefix;\n } else {\n delete (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ];\n }\n};\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\";\nimport { safeURL } from \"../server-helpers.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 // The main plugin entry statically imports Vite, so loadRPCConfig is\n // imported lazily: function bundles that never call attachRPC (e.g.\n // serverless functions) keep Vite out of the bundle (or externalized).\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n 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 const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart\n ? (req.body as Record<string, unknown>)\n : isJSON\n ? req.body\n : isUrlEncoded\n ? (req.body as Record<string, unknown>)\n : 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 const isMultipart = incomingType.includes(\"multipart/form-data\");\n const isUrlEncoded = incomingType.includes(\"urlencoded\");\n try {\n const data = isMultipart\n ? { raw: body }\n : isUrlEncoded\n ? Object.fromEntries(new URLSearchParams(body))\n : JSON.parse(body);\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart ? (data as Record<string, unknown>) : data,\n } as BodyResult);\n } catch (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n\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 * Issues an HTTP redirect on an Express or raw Node ServerResponse.\n * Uses Express's native `res.redirect(status, location)` when an Express\n * Response is provided, otherwise writes the status code and `Location`\n * header directly on the raw `ServerResponse` (safe for Connect-compatible\n * middlewares and serverless adapters whose mock responses lack `.redirect`).\n * Defaults to `303 See Other` for convention (Post/Redirect/Get).\n * @param res - Express Response or raw Node ServerResponse\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n */\nexport const redirect = (\n res: ServerResponse | ExpressResponse,\n location: string,\n status = 303,\n): void => {\n if (isExpressResponse(res)) {\n res.redirect(status, location);\n return;\n }\n res.statusCode = status;\n res.setHeader(\"Location\", location);\n res.end();\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 = safeURL(rawUrl);\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: 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","// 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\";\nimport type { JsonValue } from \"../types.d.ts\";\nimport type { RequestEvent } from \"@thednp/rpc/server\";\nimport {\n escapeRegExp,\n formatError,\n getGlobalPrefix,\n hasContentTypeMismatch,\n provideRequestContext,\n scanForServerFiles,\n} from \"@thednp/rpc/server\";\nimport { getFunctionsForPrefix } from \"../functionsMap.ts\";\nimport {\n defaultMiddlewareOptions,\n defaultPrefix,\n defaultRPCOptions,\n} from \"../options.ts\";\nimport {\n getRequestDetails,\n getResponseDetails,\n readBody,\n redirect as expressRedirect,\n} from \"./helpers.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\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 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 (\n req: IncomingMessage | ExpressRequest,\n res: ServerResponse | ExpressResponse,\n next: Connect.NextFunction | NextFunction,\n ) => {\n const { url } = getRequestDetails(req);\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 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.serverFiles,\n scanRoot: options.scanRoot,\n } as never);\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 getFunctionsForPrefix,\n * and sends the JSON-serialized result. Handles client disconnection via abort signals.\n * Supports multi-prefix setups where different middleware instances can route to functions\n * registered under different prefixes.\n * @param initialOptions - Options including rpcPrefix for URL routing and prefix-scoped function lookup\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 prefix = rpcPrefix || getGlobalPrefix() || defaultPrefix;\n\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 (\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 // Look up function in the prefix-scoped map\n const serverFunctionsForPrefix = getFunctionsForPrefix(prefix);\n const serverFunction = serverFunctionsForPrefix.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 if (raw) {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n sendResponse(400, { 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 req.headers[\"content-type\"],\n )\n ) {\n sendResponse(415, { error: UNSUPPORTED_MEDIA_TYPE });\n return;\n }\n const body = await readBody(req);\n args = Array.isArray(body.data)\n ? (body.data as JsonValue[])\n : [body.data as JsonValue];\n }\n // ─── Dispatch ────────────────────────────────────────────────────\n // Establish the per-request context around the *entire* dispatch so\n // server functions (and async continuations spawned by their work)\n // read `getRequestContext()` and can call the framework-level\n // `redirect(location)`. The adapter-specific redirect is bound into the\n // context here; `serverFunction.handler` must be invoked inside the\n // context callback so its async increments run with the context live.\n const requestEvent: RequestEvent = {\n request: req,\n response: res,\n nativeEvent: { req, res },\n locals: (res as ExpressResponse).locals ?? {},\n functionName,\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n expressRedirect(res, location, status);\n },\n send: (status, body, headers) => {\n requestEvent.sent = { status, body, headers };\n const details = getResponseDetails(res);\n if (headers) {\n for (const [name, value] of Object.entries(headers)) {\n details.setHeader(name, value);\n }\n }\n details.sendResponse(status, body);\n },\n };\n\n const { data, cancel } = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n req.on(\"close\", onClose);\n const result = await data;\n 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 adapter already wrote the\n // response. Express may also have ended the response via headersSent.\n // istanbul ignore else\n if (\n !requestEvent.redirected &&\n !requestEvent.sent &&\n !res.headersSent\n ) {\n sendResponse(200, { data: result });\n }\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n sendResponse(500, formatError(err, isProduction));\n }\n },\n });\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"express.mjs","names":[],"sources":["../../src/options.ts","../../src/functionsMap.ts","../../src/constants.ts","../../src/server-helpers.ts","../../src/express/helpers.ts","../../src/express/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 =\n ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n\n/** Error template for duplicate server function names across files. @param name - The duplicate registered name */\nexport const DUPLICATE_FUNCTION_NAME = (name: string) =>\n `Duplicate server function \"${name}\" detected. Each server function must have a unique name. Remove or rename the duplicate.`;\n","/** @module Server-side utilities. Exports the `RPCError` class for typed server-side errors, `formatError` for middleware error responses, `isFormContentType` and `hasContentTypeMismatch` for content-type validation, and `walkGlobFiles` for recursively discovering `*.server.*` files. Never import this module in client code — it is server-only. */\nimport type { ContentType, JsonObject, JsonValue } from \"./types.d.ts\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { INTERNAL_SERVER_ERROR } from \"./constants.ts\";\n\nconst GLOB_REGEX = /^.+\\.server\\.(ts|js|mjs|mts)$/;\n\n/**\n * Recursively walks `dir` and collects absolute paths to files whose\n * basename matches the `*.server.{ts,js,mjs,mts}` glob pattern.\n */\nexport const walkGlobFiles = async (dir: string): Promise<string[]> => {\n const results: string[] = [];\n const stack = [dir];\n while (stack.length) {\n const current = stack.pop()!;\n let entries;\n try {\n entries = await readdir(current, { withFileTypes: true });\n } catch (_e) {\n continue;\n }\n for (const entry of entries) {\n const fullPath = join(current, entry.name);\n if (entry.isFile() && GLOB_REGEX.test(entry.name)) {\n results.push(fullPath);\n } else if (entry.isDirectory()) {\n stack.push(fullPath);\n }\n }\n }\n return results;\n};\n\n/**\n * A typed error thrown from server functions.\n * The middleware serializes the `message` and `code` in the response,\n * allowing clients to recognise and handle specific error conditions.\n */\nexport class RPCError extends Error {\n /** Machine-readable error code (e.g. \"VALIDATION_FAILED\", \"UNAUTHORIZED\") */\n code: string;\n /** Optional diagnostic payload */\n data?: JsonValue;\n constructor(message: string, code = \"INTERNAL\", data?: JsonValue) {\n super(message);\n this.name = \"RPCError\";\n this.code = code;\n this.data = data;\n }\n}\n\n/**\n * Formats an error for the RPC middleware response.\n * In development the full `RPCError` payload is included so developers\n * can quickly identify issues. Unexpected exceptions never expose their\n * message — only the generic \"Internal Server Error\" is sent, preventing\n * information disclosure; server-side diagnostics are preserved via the\n * middleware's `console.error` logging.\n */\nexport const formatError = (\n err: unknown,\n isProduction: boolean,\n): JsonObject => {\n if (isProduction) {\n return { error: INTERNAL_SERVER_ERROR };\n }\n if (err instanceof RPCError) {\n const payload: JsonObject = {\n error: err.message || INTERNAL_SERVER_ERROR,\n code: err.code,\n };\n if (err.data !== undefined) payload.data = err.data;\n return payload;\n }\n return { error: INTERNAL_SERVER_ERROR };\n};\n\n/**\n * Checks whether a content type maps to a form encoding\n * (`multipart/form-data` or `application/x-www-form-urlencoded`).\n * Form-declared functions accept either encoding so native browser\n * submissions (urlencoded) keep working without JavaScript.\n */\nexport const isFormContentType = (contentType: string): boolean =>\n contentType === \"multipart/form-data\" ||\n contentType === \"application/x-www-form-urlencoded\";\n\n/**\n * Detects whether an incoming request's `Content-Type` header conflicts\n * with the function's declared content type. JSON and text functions are\n * enforced strictly (exact match wins), while form functions accept both\n * form encodings because the nojs fallback submits urlencoded forms to\n * multipart-declared endpoints. Requests without a `Content-Type` header\n * (curl, GET, legacy clients) are exempt from enforcement.\n * @param declared - The declared `contentType` from the server function options\n * @param rawHeader - The raw `Content-Type` request header, if present\n */\nexport const hasContentTypeMismatch = (\n declared: ContentType,\n rawHeader: string | undefined,\n): boolean => {\n // No Content-Type header → exempt (url bar, GET, curl compatibility)\n if (!rawHeader) return false;\n // Strip parameters (charset, boundary) before comparison\n const incomingType = rawHeader.trim().toLowerCase().split(\";\")[0].trim();\n if (isFormContentType(declared)) {\n // Forms: reject only non-form encodings (lenient between the two)\n return !isFormContentType(incomingType);\n }\n return incomingType !== declared;\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\nconst SAFE_URL_BASE = \"http://localhost\";\n\n/**\n * Parses a raw request URL against a fixed base without ever throwing.\n * Malformed request-targets (e.g. `/\\`, `//`, `/\\/`) make the WHATWG URL\n * parser throw `TypeError: Invalid URL`; the adapters call this while\n * building the per-request URL **before** their dispatch `try` block, so an\n * unhandled rejection there crashes raw `node:http` hosts (and Express 4).\n * On failure we fall back to the base root: the resulting pathname never\n * matches the RPC prefix, so the request is treated as non-RPC and falls\n * through to `next()` / 404 instead of crashing the process.\n * @param rawUrl - Raw request URL (path + optional query string)\n * @param base - Optional base URL, defaults to a fixed localhost origin\n * @returns A URL object; never throws\n */\nexport const safeURL = (rawUrl: string, base = SAFE_URL_BASE): URL => {\n try {\n return new URL(rawUrl, base);\n } catch {\n return new URL(\"/\", base);\n }\n};\n\nconst globalPrefixSymbol = Symbol.for(\"thednp.rpc.globalPrefix\");\n\n/** Global rpcPrefix from the last loaded config / middleware — fallback for functions without explicit prefix. */\nexport const getGlobalPrefix = (): string | undefined =>\n (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ];\n\nexport const setGlobalPrefix = (prefix: string | undefined): void => {\n if (prefix) {\n (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ] = prefix;\n } else {\n delete (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ];\n }\n};\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\";\nimport { safeURL } from \"../server-helpers.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 // The main plugin entry statically imports Vite, so loadRPCConfig is\n // imported lazily: function bundles that never call attachRPC (e.g.\n // serverless functions) keep Vite out of the bundle (or externalized).\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n 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 const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart\n ? (req.body as Record<string, unknown>)\n : isJSON\n ? req.body\n : isUrlEncoded\n ? (req.body as Record<string, unknown>)\n : 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 const isMultipart = incomingType.includes(\"multipart/form-data\");\n const isUrlEncoded = incomingType.includes(\"urlencoded\");\n try {\n const data = isMultipart\n ? { raw: body }\n : isUrlEncoded\n ? Object.fromEntries(new URLSearchParams(body))\n : JSON.parse(body);\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart ? (data as Record<string, unknown>) : data,\n } as BodyResult);\n } catch (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n\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 * Issues an HTTP redirect on an Express or raw Node ServerResponse.\n * Uses Express's native `res.redirect(status, location)` when an Express\n * Response is provided, otherwise writes the status code and `Location`\n * header directly on the raw `ServerResponse` (safe for Connect-compatible\n * middlewares and serverless adapters whose mock responses lack `.redirect`).\n * Defaults to `303 See Other` for convention (Post/Redirect/Get).\n * @param res - Express Response or raw Node ServerResponse\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n */\nexport const redirect = (\n res: ServerResponse | ExpressResponse,\n location: string,\n status = 303,\n): void => {\n if (isExpressResponse(res)) {\n res.redirect(status, location);\n return;\n }\n res.statusCode = status;\n res.setHeader(\"Location\", location);\n res.end();\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 = safeURL(rawUrl);\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: 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","// 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\";\nimport type { JsonValue } from \"../types.d.ts\";\nimport type { RequestEvent } from \"@thednp/rpc/server\";\nimport {\n escapeRegExp,\n formatError,\n getGlobalPrefix,\n hasContentTypeMismatch,\n provideRequestContext,\n scanForServerFiles,\n} from \"@thednp/rpc/server\";\nimport { getFunctionsForPrefix } from \"../functionsMap.ts\";\nimport {\n defaultMiddlewareOptions,\n defaultPrefix,\n defaultRPCOptions,\n} from \"../options.ts\";\nimport {\n getRequestDetails,\n getResponseDetails,\n readBody,\n redirect as expressRedirect,\n} from \"./helpers.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\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 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 (\n req: IncomingMessage | ExpressRequest,\n res: ServerResponse | ExpressResponse,\n next: Connect.NextFunction | NextFunction,\n ) => {\n const { url } = getRequestDetails(req);\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 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.serverFiles,\n scanRoot: options.scanRoot,\n } as never);\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 getFunctionsForPrefix,\n * and sends the JSON-serialized result. Handles client disconnection via abort signals.\n * Supports multi-prefix setups where different middleware instances can route to functions\n * registered under different prefixes.\n * @param initialOptions - Options including rpcPrefix for URL routing and prefix-scoped function lookup\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 prefix = rpcPrefix || getGlobalPrefix() || defaultPrefix;\n\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 (\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 // Look up function in the prefix-scoped map\n const serverFunctionsForPrefix = getFunctionsForPrefix(prefix);\n const serverFunction = serverFunctionsForPrefix.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 if (raw) {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n sendResponse(400, { 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 req.headers[\"content-type\"],\n )\n ) {\n sendResponse(415, { error: UNSUPPORTED_MEDIA_TYPE });\n return;\n }\n const body = await readBody(req);\n args = Array.isArray(body.data)\n ? (body.data as JsonValue[])\n : [body.data as JsonValue];\n }\n // ─── Dispatch ────────────────────────────────────────────────────\n // Establish the per-request context around the *entire* dispatch so\n // server functions (and async continuations spawned by their work)\n // read `getRequestContext()` and can call the framework-level\n // `redirect(location)`. The adapter-specific redirect is bound into the\n // context here; `serverFunction.handler` must be invoked inside the\n // context callback so its async increments run with the context live.\n const requestEvent: RequestEvent = {\n request: req,\n response: res,\n nativeEvent: { req, res },\n locals: (res as ExpressResponse).locals ?? {},\n functionName,\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n expressRedirect(res, location, status);\n },\n send: (status, body, headers) => {\n requestEvent.sent = { status, body, headers };\n const details = getResponseDetails(res);\n if (headers) {\n for (const [name, value] of Object.entries(headers)) {\n details.setHeader(name, value);\n }\n }\n details.sendResponse(status, body);\n },\n };\n\n const { data, cancel } = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n req.on(\"close\", onClose);\n const result = await data;\n 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 adapter already wrote the\n // response. Express may also have ended the response via headersSent.\n // istanbul ignore else\n if (\n !requestEvent.redirected &&\n !requestEvent.sent &&\n !res.headersSent\n ) {\n sendResponse(200, { data: result });\n }\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n sendResponse(500, formatError(err, isProduction));\n }\n },\n });\n};\n"],"mappings":"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"}
@@ -58,7 +58,7 @@ interface FastifyMiddlewareHooks {
58
58
  * @param initialOptions - Options for rpcPrefix, path matching, and the handler function
59
59
  * @returns A Fastify preHandler hook function
60
60
  */
61
- declare const createMiddleware: FastifyMiddlewareFn;
61
+ export declare const createMiddleware: FastifyMiddlewareFn;
62
62
  /**
63
63
  * Creates the Fastify RPC middleware that routes incoming requests to registered server functions.
64
64
  * Wraps the generic createMiddleware with the RPC handler that reads the body, dispatches
@@ -66,7 +66,7 @@ declare const createMiddleware: FastifyMiddlewareFn;
66
66
  * @param initialOptions - Options including rpcPrefix for URL routing
67
67
  * @returns A Fastify preHandler hook function
68
68
  */
69
- declare const createRPCMiddleware: FastifyMiddlewareFn;
69
+ export declare const createRPCMiddleware: FastifyMiddlewareFn;
70
70
  //#endregion
71
71
  //#region src/types.d.ts
72
72
  /**
@@ -111,21 +111,39 @@ type JsonValue = JsonPrimitive | JsonArray | JsonObject;
111
111
  * Dynamically imports loadRPCConfig and registers the fastify-rpc plugin.
112
112
  * @param app - Fastify instance
113
113
  */
114
- declare function attachRPC(app: FastifyInstance): Promise<void>;
114
+ export declare function attachRPC(app: FastifyInstance): Promise<void>;
115
115
  /**
116
116
  * Attaches Vite's dev server middlewares to a Fastify instance for development mode.
117
117
  * Uses an `onRequest` hook to delegate to Vite's connect-compatible middleware stack.
118
118
  * @param app - Fastify instance
119
119
  * @param vite - Running Vite dev server
120
120
  */
121
- declare function attachVite(app: FastifyInstance, vite: ViteDevServer): void;
121
+ export declare function attachVite(app: FastifyInstance, vite: ViteDevServer): void;
122
+ /**
123
+ * Creates a Fastify `onRequest` hook handler that delegates to Vite's
124
+ * connect-compatible middleware stack. Use with `app.addHook("onRequest", ...)`.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * import Fastify from "fastify";
129
+ * import { createServer } from "vite";
130
+ * import { viteMiddleware } from "@thednp/rpc/fastify";
131
+ *
132
+ * const app = Fastify();
133
+ * const vite = await createServer({ server: { middlewareMode: true } });
134
+ * app.addHook("onRequest", viteMiddleware(vite));
135
+ * ```
136
+ * @param vite - Running Vite dev server
137
+ * @returns A Fastify `onRequest` hook handler
138
+ */
139
+ export declare function viteMiddleware(vite: ViteDevServer): (request: FastifyRequest$1, reply: FastifyReply$1) => Promise<void>;
122
140
  /**
123
141
  * Reads and parses the HTTP request body from a Fastify request.
124
142
  * If Fastify's body parser already consumed the stream, uses the pre-parsed body from `req.body`.
125
143
  * @param req - Fastify request object
126
144
  * @returns A promise resolving to the parsed body with its content type
127
145
  */
128
- declare const readBody: (req: FastifyRequest$1) => Promise<BodyResult>;
146
+ export declare const readBody: (req: FastifyRequest$1) => Promise<BodyResult>;
129
147
  /**
130
148
  * Issues an HTTP redirect on a Fastify reply using the native
131
149
  * `reply.redirect(location, status)` API (Fastify v5 signature: destination
@@ -135,7 +153,7 @@ declare const readBody: (req: FastifyRequest$1) => Promise<BodyResult>;
135
153
  * @param location - The URL to redirect to
136
154
  * @param status - HTTP status code, defaults to 303
137
155
  */
138
- declare const redirect: (reply: FastifyReply$1, location: string, status?: number) => void;
156
+ export declare const redirect: (reply: FastifyReply$1, location: string, status?: number) => void;
139
157
  //#endregion
140
- export { type Fastify, type FastifyMiddlewareFn, type FastifyMiddlewareHooks, type FastifyMiddlewareOptions, type FastifyPlugin, type FastifyRPCPlugin, type FastifyReply, type FastifyRequest, type RegisteredFastifyRPCPlugin, type RpcFastifyPluginOptions, attachRPC, attachVite, createMiddleware, createRPCMiddleware, readBody, redirect };
158
+ export type { Fastify, FastifyMiddlewareFn, FastifyMiddlewareHooks, FastifyMiddlewareOptions, FastifyPlugin, FastifyRPCPlugin, FastifyReply, FastifyRequest, RegisteredFastifyRPCPlugin, RpcFastifyPluginOptions };
141
159
  //# 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":";;;;;;;;;;;;;;;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,kBACL,KAAK,gBACL,MAAM,4BACH;;;;;;;;;;cClBM,kBAAkB;;;;;;;;cAuFlB,qBAAqB;;;;;;KCtDtB;EACN;EAAiC,MAAM;;EACvC;EAA2B;;EAE7B;EACA,MAAM;;EAEJ;EAAoC,MAAM;;;;;;KAmCpC;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,aAAa,WAAW;;;;KAIxB,YAAY,gBAAgB,YAAY;;;;;;;;iBCzH9B,UAAU,KAAK,kBAAe;;;;;;;iBAepC,WAAW,KAAK,iBAAiB,MAAM;;;;;;;cAgB1C,WAAQ,KACd,qBACJ,QAAQ;;;;;;;;;;cAoFE,WAAQ,OACZ,gBAAY,kBACH"}
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,kBACL,KAAK,gBACL,MAAM,4BACH;;;;;;;;;;qBClBM,kBAAkB;;;;;;;;qBAuFlB,qBAAqB;;;;;;KCtDtB;EACN;EAAiC,MAAM;;EACvC;EAA2B;;EAE7B;EACA,MAAM;;EAEJ;EAAoC,MAAM;;;;;;KAmCpC;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,aAAa,WAAW;;;;KAIxB,YAAY,gBAAgB,YAAY;;;;;;;;wBCzH9B,UAAU,KAAK,kBAAe;;;;;;;wBAepC,WAAW,KAAK,iBAAiB,MAAM;;;;;;;;;;;;;;;;;;wBA2BvC,eACd,MAAM,iBACJ,SAAS,kBAAgB,OAAO,mBAAiB;;;;;;;qBAgBxC,WAAQ,KACd,qBACJ,QAAQ;;;;;;;;;;qBAoFE,WAAQ,OACZ,gBAAY,kBACH"}
@@ -88,6 +88,32 @@ function attachVite(app, vite) {
88
88
  });
89
89
  }
90
90
  /**
91
+ * Creates a Fastify `onRequest` hook handler that delegates to Vite's
92
+ * connect-compatible middleware stack. Use with `app.addHook("onRequest", ...)`.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * import Fastify from "fastify";
97
+ * import { createServer } from "vite";
98
+ * import { viteMiddleware } from "@thednp/rpc/fastify";
99
+ *
100
+ * const app = Fastify();
101
+ * const vite = await createServer({ server: { middlewareMode: true } });
102
+ * app.addHook("onRequest", viteMiddleware(vite));
103
+ * ```
104
+ * @param vite - Running Vite dev server
105
+ * @returns A Fastify `onRequest` hook handler
106
+ */
107
+ function viteMiddleware(vite) {
108
+ return async (request, reply) => {
109
+ reply.hijack();
110
+ await new Promise((resolve, reject) => {
111
+ const next = (err) => err ? reject(err) : resolve();
112
+ vite.middlewares(request.raw, reply.raw, next);
113
+ });
114
+ };
115
+ }
116
+ /**
91
117
  * Reads and parses the HTTP request body from a Fastify request.
92
118
  * If Fastify's body parser already consumed the stream, uses the pre-parsed body from `req.body`.
93
119
  * @param req - Fastify request object
@@ -298,6 +324,6 @@ const createRPCMiddleware = (initialOptions = {}) => {
298
324
  });
299
325
  };
300
326
  //#endregion
301
- export { attachRPC, attachVite, createMiddleware, createRPCMiddleware, readBody, redirect };
327
+ export { attachRPC, attachVite, createMiddleware, createRPCMiddleware, readBody, redirect, viteMiddleware };
302
328
 
303
329
  //# sourceMappingURL=fastify.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"fastify.mjs","names":["fastifyRpcPlugin"],"sources":["../../src/options.ts","../../src/functionsMap.ts","../../src/constants.ts","../../src/fastify/plugin.ts","../../src/fastify/helpers.ts","../../src/fastify/createMiddleware.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions: 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","/** @module Fastify plugin. Exports the RPC plugin wrapped with `fastify-plugin` for lifecycle-compatible registration. */\n// inspired by https://github.com/royalswe/vike-fastify-boilerplate/blob/main/server/index.ts\nimport fp from \"fastify-plugin\";\nimport type { MiddlewareOptions } from \"../types.d.ts\";\nimport type {\n FastifyRPCPlugin,\n RegisteredFastifyRPCPlugin,\n} from \"./types.d.ts\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\nexport type { MiddlewareOptions };\n\nconst RpcPlugin: FastifyRPCPlugin = (\n fastify,\n initialOptions,\n done,\n) => {\n // Register RPC middleware as preHandler hook\n const rpcMiddleware = createRPCMiddleware(initialOptions);\n fastify.addHook(\"preHandler\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n rpcMiddleware(request, reply, resolve);\n });\n await next();\n });\n\n done();\n};\n\n// Export the plugin wrapped with fastify-plugin\nconst rpcPlugin = fp(RpcPlugin, {\n name: \"uni-rpc-fastify-plugin\",\n}) as RegisteredFastifyRPCPlugin;\n\nexport { rpcPlugin as default };\n","// @thednp/rpc/src/fastify/helpers.ts\nimport type { FastifyReply, FastifyRequest } from \"fastify\";\nimport type { ViteDevServer } from \"vite\";\nimport type { FastifyInstance } from \"fastify\";\nimport type { Buffer } from \"node:buffer\";\nimport type { BodyResult, JsonValue } from \"../types.d.ts\";\nimport fastifyRpcPlugin from \"./plugin.ts\";\n\n/**\n * Convenience function to load RPC config and register the RPC plugin to a Fastify instance.\n * Dynamically imports loadRPCConfig and registers the fastify-rpc plugin.\n * @param app - Fastify instance\n */\nexport async function attachRPC(app: FastifyInstance) {\n // The main plugin entry statically imports Vite, so loadRPCConfig is\n // imported lazily: function bundles that never call attachRPC (e.g.\n // serverless functions) keep Vite out of the bundle (or externalized).\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n await app.register(fastifyRpcPlugin, options);\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Fastify instance for development mode.\n * Uses an `onRequest` hook to delegate to Vite's connect-compatible middleware stack.\n * @param app - Fastify instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: FastifyInstance, vite: ViteDevServer) {\n app.addHook(\"onRequest\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n vite.middlewares(request.raw, reply.raw, resolve);\n });\n await next();\n });\n}\n\n/**\n * Reads and parses the HTTP request body from a Fastify request.\n * If Fastify's body parser already consumed the stream, uses the pre-parsed body from `req.body`.\n * @param req - Fastify request object\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n req: FastifyRequest,\n): Promise<BodyResult> => {\n return new Promise((resolve, reject) => {\n const contentType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const reqBody = req.body as JsonValue;\n\n if (reqBody !== undefined) {\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart\n ? (reqBody as Record<string, unknown>)\n : isJSON\n ? reqBody\n : isUrlEncoded\n ? (reqBody as Record<string, unknown>)\n : String(reqBody),\n } as BodyResult);\n return;\n }\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n req.raw[method](\"data\", onData);\n req.raw[method](\"end\", onEnd);\n req.raw[method](\"error\", onError);\n };\n\n let body = \"\";\n\n const onData = (chunk: Buffer) => {\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n try {\n const data = isMultipart\n ? { raw: body }\n : isUrlEncoded\n ? Object.fromEntries(new URLSearchParams(body))\n : JSON.parse(body);\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart ? (data as Record<string, unknown>) : data,\n } as BodyResult);\n } catch (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n const onError = (err: Error) => {\n toggleListeners();\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n\n/**\n * Issues an HTTP redirect on a Fastify reply using the native\n * `reply.redirect(location, status)` API (Fastify v5 signature: destination\n * URL first, status code optional). Defaults to `303 See Other` for\n * convention (Post/Redirect/Get).\n * @param reply - Fastify reply object\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n */\nexport const redirect = (\n reply: FastifyReply,\n location: string,\n status = 303,\n): void => {\n reply.redirect(location, status);\n};\n","// src/fastify/createMiddleware.ts\nimport type {\n FastifyReply,\n FastifyRequest,\n HookHandlerDoneFunction,\n} from \"fastify\";\nimport type {\n FastifyMiddlewareFn,\n FastifyMiddlewareOptions,\n} from \"./types.d.ts\";\nimport type { JsonValue } from \"@thednp/rpc\";\nimport type { RequestEvent } from \"@thednp/rpc/server\";\nimport {\n escapeRegExp,\n formatError,\n getGlobalPrefix,\n hasContentTypeMismatch,\n provideRequestContext,\n safeURL,\n scanForServerFiles,\n} from \"@thednp/rpc/server\";\nimport { getFunctionsForPrefix } from \"../functionsMap.ts\";\nimport {\n defaultMiddlewareOptions,\n defaultPrefix,\n defaultRPCOptions,\n} from \"../options.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\";\nimport { readBody, redirect as fastifyRedirect } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Fastify preHandler hook with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Fastify preHandler hook function\n */\nexport const createMiddleware: FastifyMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n const middlewareName = options.name;\n 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 (\n req: FastifyRequest,\n reply: FastifyReply,\n done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = safeURL(req.url);\n const url = reqUrl.pathname;\n\n // No need to continue when no handler provided\n if (!handler) {\n done();\n return;\n }\n\n // Path matching\n if (pathMatcher && !pathMatcher.test(url)) {\n done();\n return;\n }\n\n // rpcPrefix matching (boundary-safe via escaped regex)\n if (prefixRegex && !prefixRegex.test(url)) {\n done();\n return;\n }\n\n 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 // Execute handler\n await handler(req, reply, done);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Fastify RPC middleware that routes incoming requests to registered server functions.\n * Wraps the generic createMiddleware with the RPC handler that reads the body, dispatches\n * to the matching function, and sends the JSON-serialized result.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Fastify preHandler hook function\n */\nexport const createRPCMiddleware: FastifyMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const rpcPrefix = options.rpcPrefix;\n const 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 (\n req: FastifyRequest,\n reply: FastifyReply,\n _done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = safeURL(req.url);\n const url = reqUrl.pathname;\n\n // Defense-in-depth: validate prefix match via escaped regex even though\n // the outer createMiddleware gates on the same prefix already.\n // istanbul ignore if\n if (prefixRegex && !prefixRegex.test(url)) {\n return;\n }\n\n // Optional origin check: reject requests whose Origin header does not\n // match the configured origin. Requests without an Origin header\n // (curl, native clients) pass through unchecked.\n const origin = options.origin;\n const requestOrigin = req.headers.origin;\n if (origin && requestOrigin && requestOrigin !== origin) {\n reply.status(403).send({ error: REQUEST_FORBIDDEN });\n return;\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = getFunctionsForPrefix(prefix).get(functionName);\n\n if (!serverFunction) {\n reply.status(404).send({\n error: FUNCTION_NOT_FOUND,\n });\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (req.method.toUpperCase() !== method) {\n reply.status(405).send({ error: METHOD_NOT_ALLOWED });\n return;\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = reqUrl.searchParams.get(\"args\");\n if (raw) {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n reply.status(400).send({ 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 req.headers[\"content-type\"],\n )\n ) {\n reply.status(415).send({ error: UNSUPPORTED_MEDIA_TYPE });\n return;\n }\n const body = await readBody(req);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const requestEvent: RequestEvent = {\n request: req,\n response: reply,\n nativeEvent: req,\n locals: {},\n functionName,\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n fastifyRedirect(reply, 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 reply.header(name, value);\n }\n }\n reply.status(status).send(body);\n },\n };\n const { data: dataResult, cancel } = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n\n req.raw.on(\"close\", onClose);\n const data = await dataResult;\n req.raw.off(\"close\", onClose);\n\n // istanbul ignore else\n if (\n !requestEvent.redirected &&\n !requestEvent.sent &&\n !reply.raw.headersSent\n ) {\n reply.status(200).send({ data });\n }\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n reply.status(500).send(formatError(err, isProduction));\n }\n },\n });\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"fastify.mjs","names":["fastifyRpcPlugin"],"sources":["../../src/options.ts","../../src/functionsMap.ts","../../src/constants.ts","../../src/fastify/plugin.ts","../../src/fastify/helpers.ts","../../src/fastify/createMiddleware.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions: 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 =\n ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n\n/** Error template for duplicate server function names across files. @param name - The duplicate registered name */\nexport const DUPLICATE_FUNCTION_NAME = (name: string) =>\n `Duplicate server function \"${name}\" detected. Each server function must have a unique name. Remove or rename the duplicate.`;\n","/** @module Fastify plugin. Exports the RPC plugin wrapped with `fastify-plugin` for lifecycle-compatible registration. */\n// inspired by https://github.com/royalswe/vike-fastify-boilerplate/blob/main/server/index.ts\nimport fp from \"fastify-plugin\";\nimport type { MiddlewareOptions } from \"../types.d.ts\";\nimport type {\n FastifyRPCPlugin,\n RegisteredFastifyRPCPlugin,\n} from \"./types.d.ts\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\nexport type { MiddlewareOptions };\n\nconst RpcPlugin: FastifyRPCPlugin = (\n fastify,\n initialOptions,\n done,\n) => {\n // Register RPC middleware as preHandler hook\n const rpcMiddleware = createRPCMiddleware(initialOptions);\n fastify.addHook(\"preHandler\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n rpcMiddleware(request, reply, resolve);\n });\n await next();\n });\n\n done();\n};\n\n// Export the plugin wrapped with fastify-plugin\nconst rpcPlugin = fp(RpcPlugin, {\n name: \"uni-rpc-fastify-plugin\",\n}) as RegisteredFastifyRPCPlugin;\n\nexport { rpcPlugin as default };\n","// @thednp/rpc/src/fastify/helpers.ts\nimport type { FastifyReply, FastifyRequest } from \"fastify\";\nimport type { ViteDevServer } from \"vite\";\nimport type { FastifyInstance } from \"fastify\";\nimport type { Buffer } from \"node:buffer\";\nimport type { BodyResult, JsonValue } from \"../types.d.ts\";\nimport fastifyRpcPlugin from \"./plugin.ts\";\n\n/**\n * Convenience function to load RPC config and register the RPC plugin to a Fastify instance.\n * Dynamically imports loadRPCConfig and registers the fastify-rpc plugin.\n * @param app - Fastify instance\n */\nexport async function attachRPC(app: FastifyInstance) {\n // The main plugin entry statically imports Vite, so loadRPCConfig is\n // imported lazily: function bundles that never call attachRPC (e.g.\n // serverless functions) keep Vite out of the bundle (or externalized).\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n await app.register(fastifyRpcPlugin, options);\n}\n\n/**\n * Attaches Vite's dev server middlewares to a Fastify instance for development mode.\n * Uses an `onRequest` hook to delegate to Vite's connect-compatible middleware stack.\n * @param app - Fastify instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: FastifyInstance, vite: ViteDevServer) {\n app.addHook(\"onRequest\", async (request, reply) => {\n const next = () =>\n new Promise((resolve) => {\n vite.middlewares(request.raw, reply.raw, resolve);\n });\n await next();\n });\n}\n\n/**\n * Creates a Fastify `onRequest` hook handler that delegates to Vite's\n * connect-compatible middleware stack. Use with `app.addHook(\"onRequest\", ...)`.\n *\n * @example\n * ```ts\n * import Fastify from \"fastify\";\n * import { createServer } from \"vite\";\n * import { viteMiddleware } from \"@thednp/rpc/fastify\";\n *\n * const app = Fastify();\n * const vite = await createServer({ server: { middlewareMode: true } });\n * app.addHook(\"onRequest\", viteMiddleware(vite));\n * ```\n * @param vite - Running Vite dev server\n * @returns A Fastify `onRequest` hook handler\n */\nexport function viteMiddleware(\n vite: ViteDevServer,\n): (request: FastifyRequest, reply: FastifyReply) => Promise<void> {\n return async (request, reply) => {\n reply.hijack();\n await new Promise<void>((resolve, reject) => {\n const next = (err?: unknown) => (err ? reject(err) : resolve());\n vite.middlewares(request.raw, reply.raw, next);\n });\n };\n}\n\n/**\n * Reads and parses the HTTP request body from a Fastify request.\n * If Fastify's body parser already consumed the stream, uses the pre-parsed body from `req.body`.\n * @param req - Fastify request object\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n req: FastifyRequest,\n): Promise<BodyResult> => {\n return new Promise((resolve, reject) => {\n const contentType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const reqBody = req.body as JsonValue;\n\n if (reqBody !== undefined) {\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart\n ? (reqBody as Record<string, unknown>)\n : isJSON\n ? reqBody\n : isUrlEncoded\n ? (reqBody as Record<string, unknown>)\n : String(reqBody),\n } as BodyResult);\n return;\n }\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n req.raw[method](\"data\", onData);\n req.raw[method](\"end\", onEnd);\n req.raw[method](\"error\", onError);\n };\n\n let body = \"\";\n\n const onData = (chunk: Buffer) => {\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n try {\n const data = isMultipart\n ? { raw: body }\n : isUrlEncoded\n ? Object.fromEntries(new URLSearchParams(body))\n : JSON.parse(body);\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart ? (data as Record<string, unknown>) : data,\n } as BodyResult);\n } catch (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n const onError = (err: Error) => {\n toggleListeners();\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n\n/**\n * Issues an HTTP redirect on a Fastify reply using the native\n * `reply.redirect(location, status)` API (Fastify v5 signature: destination\n * URL first, status code optional). Defaults to `303 See Other` for\n * convention (Post/Redirect/Get).\n * @param reply - Fastify reply object\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n */\nexport const redirect = (\n reply: FastifyReply,\n location: string,\n status = 303,\n): void => {\n reply.redirect(location, status);\n};\n","// src/fastify/createMiddleware.ts\nimport type {\n FastifyReply,\n FastifyRequest,\n HookHandlerDoneFunction,\n} from \"fastify\";\nimport type {\n FastifyMiddlewareFn,\n FastifyMiddlewareOptions,\n} from \"./types.d.ts\";\nimport type { JsonValue } from \"@thednp/rpc\";\nimport type { RequestEvent } from \"@thednp/rpc/server\";\nimport {\n escapeRegExp,\n formatError,\n getGlobalPrefix,\n hasContentTypeMismatch,\n provideRequestContext,\n safeURL,\n scanForServerFiles,\n} from \"@thednp/rpc/server\";\nimport { getFunctionsForPrefix } from \"../functionsMap.ts\";\nimport {\n defaultMiddlewareOptions,\n defaultPrefix,\n defaultRPCOptions,\n} from \"../options.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\";\nimport { readBody, redirect as fastifyRedirect } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates a Fastify preHandler hook with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns A Fastify preHandler hook function\n */\nexport const createMiddleware: FastifyMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n const middlewareName = options.name;\n 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 (\n req: FastifyRequest,\n reply: FastifyReply,\n done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = safeURL(req.url);\n const url = reqUrl.pathname;\n\n // No need to continue when no handler provided\n if (!handler) {\n done();\n return;\n }\n\n // Path matching\n if (pathMatcher && !pathMatcher.test(url)) {\n done();\n return;\n }\n\n // rpcPrefix matching (boundary-safe via escaped regex)\n if (prefixRegex && !prefixRegex.test(url)) {\n done();\n return;\n }\n\n 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 // Execute handler\n await handler(req, reply, done);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Fastify RPC middleware that routes incoming requests to registered server functions.\n * Wraps the generic createMiddleware with the RPC handler that reads the body, dispatches\n * to the matching function, and sends the JSON-serialized result.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns A Fastify preHandler hook function\n */\nexport const createRPCMiddleware: FastifyMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as FastifyMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const rpcPrefix = options.rpcPrefix;\n const 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 (\n req: FastifyRequest,\n reply: FastifyReply,\n _done: HookHandlerDoneFunction,\n ) => {\n const reqUrl = safeURL(req.url);\n const url = reqUrl.pathname;\n\n // Defense-in-depth: validate prefix match via escaped regex even though\n // the outer createMiddleware gates on the same prefix already.\n // istanbul ignore if\n if (prefixRegex && !prefixRegex.test(url)) {\n return;\n }\n\n // Optional origin check: reject requests whose Origin header does not\n // match the configured origin. Requests without an Origin header\n // (curl, native clients) pass through unchecked.\n const origin = options.origin;\n const requestOrigin = req.headers.origin;\n if (origin && requestOrigin && requestOrigin !== origin) {\n reply.status(403).send({ error: REQUEST_FORBIDDEN });\n return;\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = getFunctionsForPrefix(prefix).get(functionName);\n\n if (!serverFunction) {\n reply.status(404).send({\n error: FUNCTION_NOT_FOUND,\n });\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (req.method.toUpperCase() !== method) {\n reply.status(405).send({ error: METHOD_NOT_ALLOWED });\n return;\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = reqUrl.searchParams.get(\"args\");\n if (raw) {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n reply.status(400).send({ 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 req.headers[\"content-type\"],\n )\n ) {\n reply.status(415).send({ error: UNSUPPORTED_MEDIA_TYPE });\n return;\n }\n const body = await readBody(req);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const requestEvent: RequestEvent = {\n request: req,\n response: reply,\n nativeEvent: req,\n locals: {},\n functionName,\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n fastifyRedirect(reply, 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 reply.header(name, value);\n }\n }\n reply.status(status).send(body);\n },\n };\n const { data: dataResult, cancel } = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n\n req.raw.on(\"close\", onClose);\n const data = await dataResult;\n req.raw.off(\"close\", onClose);\n\n // istanbul ignore else\n if (\n !requestEvent.redirected &&\n !requestEvent.sent &&\n !reply.raw.headersSent\n ) {\n reply.status(200).send({ data });\n }\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n reply.status(500).send(formatError(err, isProduction));\n }\n },\n });\n};\n"],"mappings":"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"}