@thednp/rpc 0.0.1 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/AGENTS.md +5 -5
  2. package/CLAUDE.md +1 -0
  3. package/README.md +195 -47
  4. package/dist/express/express.d.mts +110 -16
  5. package/dist/express/express.d.mts.map +1 -1
  6. package/dist/express/express.mjs +113 -20
  7. package/dist/express/express.mjs.map +1 -1
  8. package/dist/fastify/fastify.d.mts +83 -5
  9. package/dist/fastify/fastify.d.mts.map +1 -1
  10. package/dist/fastify/fastify.mjs +84 -18
  11. package/dist/fastify/fastify.mjs.map +1 -1
  12. package/dist/fastify/plugin/fastify/plugin.d.mts +64 -9
  13. package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -1
  14. package/dist/fastify/plugin/fastify/plugin.mjs +73 -18
  15. package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
  16. package/dist/helpers/helpers.d.mts +66 -4
  17. package/dist/helpers/helpers.d.mts.map +1 -1
  18. package/dist/helpers/helpers.mjs +34 -7
  19. package/dist/helpers/helpers.mjs.map +1 -1
  20. package/dist/hono/hono.d.mts +75 -6
  21. package/dist/hono/hono.d.mts.map +1 -1
  22. package/dist/hono/hono.mjs +84 -24
  23. package/dist/hono/hono.mjs.map +1 -1
  24. package/dist/index.d.mts +210 -19
  25. package/dist/index.d.mts.map +1 -1
  26. package/dist/index.mjs +137 -31
  27. package/dist/index.mjs.map +1 -1
  28. package/dist/koa/koa.d.mts +52 -0
  29. package/dist/koa/koa.d.mts.map +1 -1
  30. package/dist/koa/koa.mjs +86 -16
  31. package/dist/koa/koa.mjs.map +1 -1
  32. package/dist/server/server.d.mts +111 -10
  33. package/dist/server/server.d.mts.map +1 -1
  34. package/dist/server/server.mjs +117 -17
  35. package/dist/server/server.mjs.map +1 -1
  36. package/package.json +48 -31
  37. package/wiki/adapters.md +0 -143
  38. package/wiki/best-practices.md +0 -201
  39. package/wiki/client-usage.md +0 -62
  40. package/wiki/configuration.md +0 -77
  41. package/wiki/getting-started.md +0 -76
  42. package/wiki/index.md +0 -26
  43. package/wiki/security.md +0 -54
  44. package/wiki/server-functions.md +0 -93
  45. package/wiki/setup.md +0 -77
package/AGENTS.md CHANGED
@@ -81,7 +81,7 @@ The tsdown.config.ts produces multiple entries:
81
81
 
82
82
  ## Important Notes
83
83
 
84
- - In dev mode, **only** the Vite dev server and Express/Connect middleware are available
84
+ - In dev mode, **only** the Vite dev server and Express/Connect middleware are available, which means adapters don't work in DEV mode
85
85
  - Uses `deno` for linting and formatting (not eslint/prettier)
86
86
  - Uses `tsdown` for bundling (not rollup/vite directly)
87
87
  - Uses `vitest` for testing with `istanbul` coverage
@@ -97,11 +97,11 @@ The tsdown.config.ts produces multiple entries:
97
97
 
98
98
  ## Security & Hardening
99
99
 
100
- - **Prefix boundary check**: All adapters use `new RegExp(\`^/${escapeRegExp(rpcPreffix)}/\`)` instead of `startsWith` to prevent path segment bypassing (e.g., `/__rpc-evil/foo` no longer matches prefix `"__rpc"`)
101
- - **Prefix regex injection prevention**: `rpcPreffix` config string is escaped via `escapeRegExp()` before being embedded in the boundary regex, preventing ReDoS or unintended matching from metacharacters in the prefix
100
+ - **Prefix boundary check**: All adapters use `new RegExp(\`^/${escapeRegExp(rpcPrefix)}/\`)` instead of `startsWith` to prevent path segment bypassing (e.g., `/__rpc-evil/foo` no longer matches prefix `"__rpc"`)
101
+ - **Prefix regex injection prevention**: `rpcPrefix` config string is escaped via `escapeRegExp()` before being embedded in the boundary regex, preventing ReDoS or unintended matching from metacharacters in the prefix
102
102
  - **Regex compilation hoisted**: All prefix/path regexes are compiled once at middleware creation time (not per-request), eliminating per-request regex overhead
103
103
  - **Koa URL normalization**: Koa adapter parses `ctx.url` through `new URL()` to strip query strings and normalize encoding before prefix checking
104
- - **Code injection prevention in client module generation**: `getClientModules.ts` validates all interpolated identifiers (`fnName`, `fnEntry`, `rpcPreffix`) against `/^[A-Za-z_$][A-Za-z0-9_$]*$/` (and a path-safe variant allowing `/`) before interpolating into the generated client bundle. This prevents code injection via malicious export names or prefixes containing template literal interpolations (`${...}`), backticks, or `</script>` sequences.
104
+ - **Code injection prevention in client module generation**: `getClientModules.ts` validates all interpolated identifiers (`fnName`, `fnEntry`, `rpcPrefix`) against `/^[A-Za-z_$][A-Za-z0-9_$]*$/` (and a path-safe variant allowing `/`) before interpolating into the generated client bundle. This prevents code injection via malicious export names or prefixes containing template literal interpolations (`${...}`), backticks, or `</script>` sequences.
105
105
  - **Body size limits**: Host frameworks cap parsed JSON bodies — Express (`express.json({ limit })`), Fastify (`bodyLimit`), Koa (`koa-body`), Hono (`hono/body-limit`). Rely on your framework's body parser middleware for size limits (see wiki/best-practices.md). The raw stream path in `readBody` does not impose a built-in limit — use framework middleware or a custom body limit handler for defense-in-depth.
106
106
  - **Generic 404 responses**: Error messages no longer echo the requested function name, preventing function enumeration
107
107
  - **Auth is middleware's responsibility**: Authentication should be handled by middleware registered before `createRPCMiddleware()`. The middleware chain naturally composes — no built-in auth hook is needed.
@@ -113,7 +113,7 @@ The framework's security boundary is the **RPC prefix-gated HTTP endpoint**. Inp
113
113
 
114
114
  | Input | Source | Trust Level | Hardening Applied |
115
115
  | -----------------------| -------------------------------| ---------------------| --------------------------------------------------------------------------|
116
- | `rpcPreffix` (config) | `rpc.config.ts` / dev options | Developer-trusted | Escaped before regex; validated before code gen |
116
+ | `rpcPrefix` (config) | `rpc.config.ts` / dev options | Developer-trusted | Escaped before regex; validated before code gen |
117
117
  | Function export name | `src/api/server.ts` exports | Developer-trusted | Validated against identifier regex before client codegen |
118
118
  | HTTP request URL | Untrusted client | Boundary-filtered | Prefix regex (escaped, anchored, hoisted); Koa URL normalization |
119
119
  | HTTP request body | Untrusted client | Capped by framework | Framework body parsers cap JSON and raw bodies |
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ AGENTS.md
package/README.md CHANGED
@@ -1,66 +1,146 @@
1
1
  # @thednp/rpc
2
2
 
3
- [![Coverage Status](https://coveralls.io/repos/github/thednp/rpcv/badge.svg)](https://coveralls.io/github/thednp/rpcv)
4
- [![ci](https://github.com/thednp/rpcv/actions/workflows/ci.yml/badge.svg)](https://github.com/thednp/rpcv/actions/workflows/ci.yml)
3
+ [![Coverage Status](https://coveralls.io/repos/github/thednp/rpc/badge.svg)](https://coveralls.io/github/thednp/rpc)
4
+ [![ci](https://github.com/thednp/rpc/actions/workflows/ci.yml/badge.svg)](https://github.com/thednp/rpc/actions/workflows/ci.yml)
5
5
  [![NPM Version](https://img.shields.io/npm/v/@thednp/rpc.svg)](https://www.npmjs.com/package/@thednp/rpc)
6
+ [![JSR Version](https://img.shields.io/jsr/v/@thednp/rpc.svg)](https://jsr.io/@thednp/rpc)
6
7
  [![NPM Downloads](https://img.shields.io/npm/dm/@thednp/rpc.svg)](http://npm-stat.com/charts.html?package=@thednp/rpc)
7
8
 
8
- A Vite plugin for automatic RPC generation that is simple, framework agnostic and very simple to use.
9
+ A Vite plugin for automatic RPC generation — simple, framework agnostic, and easy to use.
9
10
 
10
- * Server functions defined in `src/api/server.ts` are auto-scanned, with client modules generated at build time.
11
- * Typed client `fetch` based modules are generated and available via the RPC plugin.
11
+ ## Isomorphic Design
12
12
 
13
- The name stands for RPC via Vite, because that's exactly what it is. No more, no less.
13
+ Server functions defined in `src/api/server.ts` run exclusively on the server. The plugin transforms their imports into client-side fetch stubs, so calling a server function from the client looks and feels like a local call — but the actual execution stays on the server.
14
+
15
+ The server functions run **isomorphically** within any Vite powered runtime.
14
16
 
15
17
  ## Why this exists
16
18
 
17
- Most RPC solutions ask you to adopt a new way of thinking. You learn a complex API, you organize your code into a specific structure, for sure they are powerful, they work well and provide excelent DX, but complexity comes with its own drawbacks.
19
+ Most RPC solutions ask you to adopt a new way of thinking, require learning a complex API, some are vendor locked, some even allow you to blend in with your client code (via `"use server"` directive), for sure they are powerful and work well, they provide excellent DX, but complexity always comes with its own drawbacks.
20
+
21
+ `@thednp/rpc` carves out the niche that wants to do RPC **without the weight of an entire framework**. If your app is a Vite site, a static SPA, or a small server powered by a single middleware — but you still want typed, cancellable, server-only functions callable from the client — you shouldn't have to adopt a full meta-framework, a full-stack router, or a build-time convention just to bridge the two. This plugin gives you that bridge alone: no framework to learn, no runtime to adopt, no vendor to sign up with.
22
+
23
+ ### Simplicity is best
24
+
25
+ `@thednp/rpc` takes simplicity very seriously:
26
+ <details>
27
+ <summary><b>Server functions should just be functions</b></summary>
28
+
29
+ You define them in a file, import and call them where you need them. The plugin handles everything in between — system wide configuration, scanning, type inference, client stub generation, middleware registration, request cancellation — without asking you to restructure your codebase.
30
+ </details>
31
+
32
+ <details>
33
+ <summary><b>The architecture is clean and minimal</b></summary>
34
+
35
+ * `createFunction.ts` — server-side definition (wrapped handler with `AbortController`)
36
+ * `getClientModules.ts` — build-time code generation (string template with validation)
37
+ * `helpers.ts` — client-side runtime (thin `fetch` based modules)
38
+ * `scanForServerFiles.ts` — file discovery
39
+ * **Adapters** — thin middleware wrappers
40
+ </details>
41
+
42
+ ### Sound mental model
43
+
44
+ * **Query Engine** — The Brain (something like `@tanstack/react-query` that handles caching, lifecycles, deduplication).
45
+ * **@thednp/rpc** — The Nervous System (isomorphic transport, serialization, client/server bridge, request cancellation).
46
+ * **UI Framework** — The Muscle (Reactive DOM updates).
47
+
48
+ ## What you get
49
+
50
+ <details>
51
+ <summary><b>File-level server isolation, without directives</b></summary>
52
+
53
+ Your server code lives in `src/api/server.ts`. The plugin knows it's server code because of where it lives, not because you annotated it. There's no `'use server'` string to forget, no build error when you accidentally leave it out. The boundary is **the file**. That's it.
54
+ </details>
55
+
56
+ <details>
57
+ <summary><b>One config file for everything</b></summary>
58
+
59
+ The plugin options live in `rpc.config.ts` at your project root. Adapter choice, URL prefix, middleware hooks — it's all in one place. You set it up once and then you don't think about it again.
60
+
61
+ You can access config system wide by calling `loadRPCConfig()` within your project server-side code.
62
+ </details>
63
+
18
64
 
19
- `@thednp/rpc` takes the opposite bet: your **server functions should just be functions**. You define them in a file, import and call them where you need them. The plugin handles everything in between — system wide configuration, scanning, type inference, client stub generation, middleware registration, request cancellation — without asking you to restructure your codebase or learn a new DSL (Domain-Specific Language).
65
+ <details>
66
+ <summary><b>Typed client modules, generated at build time</b></summary>
20
67
 
21
- ### The Mental model
22
- * **Query Engine** The Brain (something like `@tanstack/react-query` that handles caching, lifecycles, deduplication).
23
- * **@thednp/rpc** The Nervous System (transport, serialization, client/server bridge, request cancellation).
24
- * **UI Framework** = The Muscle (Reactive DOM updates).
68
+ When you import a server function on the client, the plugin generates a stub that matches your function's exact signature. Change an argument type on the server, and the client types update on the next build. There's no separate codegen command to run, no generated files to commit, no drift between your server and client types.
69
+ </details>
25
70
 
26
- ## Features
71
+ <details>
72
+ <summary><b>Cancellation should be easy</b></summary>
27
73
 
28
- - Framework-agnostic core with adapters for **Express**, **Fastify**, **Hono**, and **Koa**
29
- - Automatic RPC generation — server functions are auto-scanned; client `fetch` based modules are generated at build time
30
- - File-level server code isolation (no `'use server'` directives required)
31
- - System-wide configuration via `rpc.config.ts`
32
- - `AbortController` based request cancellation (via `.cancel()` on the returned handle)
33
- - TypeScript support with generic type inference
74
+ Every server function call returns a handle with a `cancel()` helper. Under the hood, it's an `AbortController` wired into the fetch request. You don't have to create the controller, pass the signal, or clean up listeners. You just call `cancel()` and the request dies. The server function receives the `AbortSignal` as its first argument, so you can bail out of expensive work early if the client has already moved on.
75
+ </details>
76
+
77
+ <details>
78
+ <summary><b>Your server framework is your business</b></summary>
79
+
80
+ The core plugin doesn't care whether you're running Express, Fastify, Hono, or Koa. Adapters for all four are bundled with the package — you import the one you need, register it as middleware, and you're done. If you're building a plain SPA with no server framework at all, the Vite dev server handles RPC requests directly in development. No adapter needed.
81
+ </details>
82
+
83
+ <details>
84
+ <summary><b>TypeScript throughout</b></summary>
85
+
86
+ Generic type inference flows from your server function's arguments and return type all the way to the client stub. You get autocomplete for function names, argument types, and return types without writing a single type annotation on the client side.
87
+ </details>
34
88
 
35
89
  ## Demos
36
90
 
37
- | Example | Source Code | Try online |
38
- | -----------------| ---------------------------------------------------------------------------------| -------------------------------------------------------------------------------------------|
39
- | SPA - node:http | [examples/spa](https://github.com/thednp/rpcv/tree/master/examples/spa) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpcv/tree/master/examples/spa) |
40
- | SSR - node:http | [examples/ssr](https://github.com/thednp/rpcv/tree/master/examples/ssr) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpcv/tree/master/examples/ssr) |
41
- | Express | [examples/express](https://github.com/thednp/rpcv/tree/master/examples/express) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpcv/tree/master/examples/express) |
42
- | Fastify | [examples/fastify](https://github.com/thednp/rpcv/tree/master/examples/fastify) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpcv/tree/master/examples/fastify) |
43
- | Hono | [examples/hono](https://github.com/thednp/rpcv/tree/master/examples/hono) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpcv/tree/master/examples/hono) |
44
- | Koa | [examples/koa](https://github.com/thednp/rpcv/tree/master/examples/koa) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpcv/tree/master/examples/koa) |
91
+ | Example | Source Code | Try online |
92
+ | -----------------| --------------------------------------------------------------------------------| ------------------------------------------------------------------------------------------|
93
+ | SPA - node:http | [examples/spa](https://github.com/thednp/rpc/tree/master/examples/spa) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/spa) |
94
+ | SSR - node:http | [examples/ssr](https://github.com/thednp/rpc/tree/master/examples/ssr) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/ssr) |
95
+ | Express | [examples/express](https://github.com/thednp/rpc/tree/master/examples/express) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/express) |
96
+ | Fastify | [examples/fastify](https://github.com/thednp/rpc/tree/master/examples/fastify) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/fastify) |
97
+ | Hono | [examples/hono](https://github.com/thednp/rpc/tree/master/examples/hono) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/hono) |
98
+ | Koa | [examples/koa](https://github.com/thednp/rpc/tree/master/examples/koa) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpc/tree/master/examples/koa) |
45
99
 
100
+ > **NOTE**: Stackblitz is currently working on upgrading their platform. Demos may not work properly.
46
101
 
47
102
  ## Examples
48
103
 
49
- | Example | Adapter | Type | Run Command |
50
- | ---------| -----------------------------------------------| ------| --------------------|
51
- | spa | Vite dev server (Connect, Express-compatible) | SPA | `pnpm dev` |
52
- | express | Express | SSR | `pnpm dev:express` |
53
- | fastify | Fastify | SSR | `pnpm dev:fastify` |
54
- | hono | Hono | SSR | `pnpm dev:hono` |
55
- | koa | Koa | SSR | `pnpm dev:koa` |
56
- | ssr | Custom `node:http` (Express-compatible) | SSR | `pnpm dev:ssr` |
104
+ | Example | Adapter | Type | Run Command | RPC Approach |
105
+ | ---------| -----------------------------------------------| ------| --------------------| ------------------------------------|
106
+ | spa | Vite dev server (Connect, Express-compatible) | SPA | `pnpm dev` | Client stubs only |
107
+ | express | Express | SSR | `pnpm dev:express` | Direct import (SSR) + client stubs |
108
+ | fastify | Fastify | SSR | `pnpm dev:fastify` | Direct import (SSR) + client stubs |
109
+ | hono | Hono | SSR | `pnpm dev:hono` | Direct import (SSR) + client stubs |
110
+ | koa | Koa | SSR | `pnpm dev:koa` | Direct import (SSR) + client stubs |
111
+ | ssr | Custom `node:http` (Express-compatible) | SSR | `pnpm dev:ssr` | Direct import (SSR) + client stubs |
112
+
113
+ SSR examples demonstrate isomorphic usage: server functions are imported directly during server-side rendering (`entry-server.ts`) and also called from the client via auto-generated fetch stubs. The SPA example uses only the client-side stubs.
57
114
 
58
115
  ## Quick Start
59
116
 
60
117
  ### 1. Installation
61
118
 
62
119
  ```bash
63
- pnpm add @thednp/rpc@latest
120
+ // npm/pnpm and jsr
121
+ pnpm add jsr:@thednp/rpc
122
+ // OR
123
+ npx jsr add @thednp/rpc
124
+ ```
125
+
126
+ ```bash
127
+ // deno and jsr
128
+ deno add jsr:@thednp/rpc
129
+ ```
130
+
131
+ ```bash
132
+ // pnpm/npm/bun from the npm registry
133
+ pnpm add @thednp/rpc
134
+ ```
135
+
136
+ ```bash
137
+ // npm
138
+ npm i @thednp/rpc
139
+ ```
140
+
141
+ ```bash
142
+ // bun
143
+ bun add @thednp/rpc
64
144
  ```
65
145
 
66
146
  ### 2. Configuration
@@ -72,7 +152,7 @@ import { defineConfig } from "@thednp/rpc";
72
152
 
73
153
  export default defineConfig({
74
154
  adapter: "express",
75
- rpcPreffix: "__rpc",
155
+ rpcPrefix: "__rpc",
76
156
  });
77
157
  ```
78
158
 
@@ -88,6 +168,8 @@ export default defineConfig({
88
168
 
89
169
  ```
90
170
 
171
+ Check [Configuration Guide](wiki/configuration.md) for details.
172
+
91
173
  ### 3. Define a server function
92
174
 
93
175
  Create `src/api/server.ts`:
@@ -112,9 +194,11 @@ Create `src/api/index.ts`:
112
194
  export * from "./server";
113
195
  ```
114
196
 
115
- ### 4. Call it from the client
197
+ Check [Server Functions Guide](./wiki/server-functions.md) for details.
198
+
199
+ ### 4. Call it in your code
116
200
 
117
- Import the generated client module in any client-side file:
201
+ Import the function in any client-side or server-side file:
118
202
 
119
203
  ```ts
120
204
  // src/app.ts
@@ -122,12 +206,25 @@ import { greet } from "./api";
122
206
 
123
207
  const { data, cancel } = greet("World");
124
208
  const result = await data; // "Hello, World!"
125
- cancel(); // AbortController-based cancellation
209
+ cancel("Client aborted"); // AbortController-based cancellation
126
210
  ```
127
211
 
128
212
  ### 5. Register the RPC middleware on the server
129
213
 
130
- Import and use the middleware from your chosen adapter package. See the [Adapters guide](./wiki/adapters.md) for full snippets for each framework.
214
+ Import and use the middleware from your chosen adapter package.
215
+
216
+ ```ts
217
+ // Express
218
+ import express from "express";
219
+ import { createRPCMiddleware } from "@thednp/rpc/express";
220
+
221
+ const app = express();
222
+ app.use(createRPCMiddleware());
223
+
224
+ app.listen(3000);
225
+ ```
226
+
227
+ See the [Adapters guide](./wiki/adapters.md) for full snippets for each framework.
131
228
 
132
229
  ## Testing
133
230
 
@@ -153,14 +250,65 @@ These tests check the following:
153
250
  * check if there is any issue generating the HTML
154
251
  * check if server functions work properly
155
252
 
253
+ ## Contributing
254
+
255
+ Contributions are welcome. This project uses:
256
+
257
+ - **pnpm** for package management
258
+ - **deno** for linting and formatting
259
+ - **tsdown** for bundling
260
+ - **vitest** with **istanbul** for testing
261
+ - **TypeScript** for type checking
262
+
263
+ ### Development
264
+
265
+ ```bash
266
+ pnpm lint # deno lint + tsc -noEmit
267
+ pnpm format # deno fmt src
268
+ pnpm test # Run tests with coverage
269
+ pnpm test-ui # Run tests with interactive UI
270
+ pnpm build # Bundle with tsdown
271
+ ```
272
+
273
+ All changes should pass `pnpm lint && pnpm format && pnpm test` before submitting. See [AGENTS.md](./AGENTS.md) for the full command reference and project conventions.
274
+
156
275
  ## Security
157
276
 
158
- - Prefix boundary check via anchored regex — prevents `/__rpc-evil/foo` bypass
159
- - Body size limit: `readBody` caps raw text/plain streams at 1 MiB by default
160
- - Code injection prevention: all interpolated identifiers are validated before client module generation
161
- - Generic error responses — no stack traces or internal details exposed to the client
277
+ RPC endpoints are, by definition, public surface area. Anything reachable over HTTP can be prodded, poked, and abused. We've tried to close the obvious doors:
278
+
279
+ <details>
280
+ <summary><b>Prefix boundary checking</b></summary>
281
+
282
+ The URL prefix is validated with an anchored regex, not a simple `startsWith` check. This means a request to `/__rpc-evil/foo` won't accidentally match the `/__rpc` prefix and slip through to your server functions. It sounds like a small thing, but prefix bypass bugs are one of the most common mistakes in middleware-based routing, and they're the kind of thing that only shows up in a security audit at 2am.
283
+ </details>
284
+
285
+ <details>
286
+ <summary><b>Code injection prevention</b></summary>
287
+
288
+ When the plugin generates client modules, it interpolates your function names and type signatures into the generated code. Every identifier is validated before it's written into the output. A server function named `greet; drop table users` won't make it through the generator — it'll fail at build time with a clear error, rather than producing a client module with arbitrary code in it.
289
+ </details>
290
+
291
+ <details>
292
+ <summary><b>Generic error responses</b></summary>
293
+
294
+ When a server function throws, the client receives a clean, generic error message. Stack traces, file paths, database connection strings, and other internal details stay on the server, where they belong. Your server logs get the full error. The client gets `"Internal Server Error"` and nothing more.
295
+ </details>
296
+
297
+ <details>
298
+ <summary><b>Body size limits</b></summary>
299
+
300
+ The `readBody` utility of each adapter doesn't cap raw request bodies by default. You need to use the middleware provided by your server framework of choice.
301
+ </details>
302
+
303
+ <details>
304
+ <summary><b>Method restriction (GET/POST only)</b></summary>
305
+
306
+ Server functions only support `GET` and `POST` (default `POST`). RPC dispatch is not REST — `PUT`/`PATCH`/`DELETE` carry resource semantics that don't apply to function calls, and `OPTIONS` must stay reserved for CORS preflight. Every accepted method is another dispatch path to validate; keeping the surface minimal (and defaulting to `POST`) reduces CSRF and parsing attack surface. See [Server Functions Guide](./wiki/server-functions.md) for details.
307
+ </details>
308
+
309
+ ---
310
+ The full threat model, including edge cases and configuration options for tightening things further, is documented in [Security](./wiki/security.md).
162
311
 
163
- See [Security](./wiki/security.md) for full details.
164
312
 
165
313
  ## Documentation
166
314
 
@@ -175,4 +323,4 @@ See [Security](./wiki/security.md) for full details.
175
323
 
176
324
  ## License
177
325
 
178
- Released under [MIT](LICENSE).
326
+ Released under [MIT](./LICENSE).
@@ -1,39 +1,133 @@
1
1
  import { Connect, ViteDevServer } from "vite";
2
2
  import { BodyResult, JsonValue, MiddlewareOptions, RpcPluginOptions } from "@thednp/rpc";
3
- import { IncomingMessage, ServerResponse } from "node:http";
3
+ import { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
4
4
  import { Express, NextFunction, Request, Response } from "express";
5
5
  //#region src/express/types.d.ts
6
+ /**
7
+ * Express-specific middleware options, constrained to the `"express"` adapter.
8
+ */
6
9
  type ExpressMiddlewareOptions = MiddlewareOptions<"express">;
10
+ /**
11
+ * Express middleware factory: takes optional initial options and returns
12
+ * the Express/Connect-compatible handler.
13
+ */
7
14
  type ExpressMiddlewareFn = <A extends RpcPluginOptions["adapter"] = "express">(initialOptions?: Partial<ExpressMiddlewareOptions>) => ExpressMiddlewareHooks["handler"];
15
+ /**
16
+ * Express/Connect middleware handler signature used by the RPC middleware.
17
+ */
8
18
  interface ExpressMiddlewareHooks {
19
+ /**
20
+ * The handler invoked for each matched request.
21
+ * @param req - Node or Express request object
22
+ * @param res - Node or Express response object
23
+ * @param next - Connect or Express next function
24
+ */
9
25
  handler: (req: IncomingMessage | Request, res: ServerResponse | Response, next: Connect.NextFunction | NextFunction) => Promise<void>;
10
26
  }
27
+ /**
28
+ * Wraps a server response to normalize status, header, and send operations
29
+ * across Node `ServerResponse` and Express `Response` objects.
30
+ */
31
+ type ResponseDetails = {
32
+ /** Whether the response was already sent */
33
+ isResponseSent: boolean;
34
+ /** Sets a response header */
35
+ setHeader: (name: string, value: string) => void;
36
+ /** Current response status code */
37
+ statusCode: number;
38
+ /** Sets the response status code */
39
+ setStatusCode: (code: number) => void;
40
+ /** Sends a JSON response with the given status code and output */
41
+ sendResponse: (code: number, output: Record<string, JsonValue>) => void;
42
+ };
43
+ /**
44
+ * Normalized view of an incoming request: URL parts, headers, and method.
45
+ */
46
+ type RequestDetails = {
47
+ /** Full request URL (path + query string) */
48
+ url: string;
49
+ /** Query string including the leading `?` */
50
+ search: string;
51
+ /** Parsed query string parameters */
52
+ searchParams: URLSearchParams;
53
+ /** Raw request headers */
54
+ headers: IncomingHttpHeaders;
55
+ /** HTTP method (GET, POST, etc.) */
56
+ method: string | undefined;
57
+ };
11
58
  //#endregion
12
59
  //#region src/express/createMiddleware.d.ts
60
+ /**
61
+ * Creates an Express middleware with optional path and rpcPrefix filtering.
62
+ * Middleware names are deduplicated — reusing a name throws an error.
63
+ * Prefix and path regexes are compiled once at creation time (hoisted) for performance.
64
+ * @param initialOptions - Options for rpcPrefix, path matching, and the handler function
65
+ * @returns An Express middleware function
66
+ */
13
67
  declare const createMiddleware: ExpressMiddlewareFn;
68
+ /**
69
+ * Creates the Express RPC middleware that routes incoming requests to registered server functions.
70
+ * Reads the request body, dispatches to the matching function via serverFunctionsMap,
71
+ * and sends the JSON-serialized result. Handles client disconnection via abort signals.
72
+ * @param initialOptions - Options including rpcPrefix for URL routing
73
+ * @returns An Express middleware function
74
+ */
14
75
  declare const createRPCMiddleware: ExpressMiddlewareFn;
15
76
  //#endregion
16
77
  //#region src/express/helpers.d.ts
78
+ /**
79
+ * Convenience function to load RPC config and attach the RPC middleware to an Express app.
80
+ * Dynamically imports loadRPCConfig and creates the middleware with loaded options.
81
+ * @param app - Express application instance
82
+ */
17
83
  declare function attachRPC(app: Express): Promise<void>;
84
+ /**
85
+ * Attaches Vite's dev server middlewares to an Express app for development mode.
86
+ * @param app - Express application instance
87
+ * @param vite - Running Vite dev server
88
+ */
18
89
  declare function attachVite(app: Express, vite: ViteDevServer): void;
90
+ /**
91
+ * Reads and parses the HTTP request body from an Express or Node IncomingMessage.
92
+ * If a body parser middleware (e.g. express.json()) already consumed the stream,
93
+ * uses the pre-parsed body from `req.body`.
94
+ * @param req - Express or Node.js IncomingMessage
95
+ * @returns A promise resolving to the parsed body with its content type
96
+ */
19
97
  declare const readBody: (req: Request | IncomingMessage) => Promise<BodyResult>;
98
+ /**
99
+ * Type guard that checks whether a request is an Express Request (has `originalUrl`).
100
+ * @param req - A Node IncomingMessage or Express Request
101
+ * @returns True if the request is an Express Request
102
+ */
20
103
  declare const isExpressRequest: (req: IncomingMessage | Request) => req is Request;
104
+ /**
105
+ * Type guard that checks whether a response is an Express Response (has `json` and `send` methods).
106
+ * @param res - A Node ServerResponse or Express Response
107
+ * @returns True if the response is an Express Response
108
+ */
21
109
  declare const isExpressResponse: (res: ServerResponse | Response) => res is Response;
110
+ /**
111
+ * Type guard that checks whether a request has a pre-parsed body (`body` property).
112
+ * Used to detect if a body-parser middleware already consumed the stream.
113
+ * @param req - A Node IncomingMessage or Express Request
114
+ * @returns True if the request has a body property
115
+ */
22
116
  declare const hasPreParsedBody: (req: IncomingMessage | Request) => req is Request;
23
- declare const getRequestDetails: (request: Request | IncomingMessage) => {
24
- url: string;
25
- search: string;
26
- searchParams: URLSearchParams;
27
- headers: import("node:http").IncomingHttpHeaders;
28
- method: string | undefined;
29
- };
30
- declare const getResponseDetails: (response: Response | ServerResponse) => {
31
- isResponseSent: boolean;
32
- setHeader: (name: string, value: string) => void;
33
- statusCode: number;
34
- setStatusCode: (code: number) => void;
35
- sendResponse: (code: number, output: Record<string, JsonValue>) => void;
36
- };
117
+ /**
118
+ * Extracts normalized request details from an Express or Node IncomingMessage.
119
+ * Parses the URL to extract pathname, search string, and search params.
120
+ * @param request - Express or Node.js request object
121
+ * @returns Normalized request details including URL, headers, and method
122
+ */
123
+ declare const getRequestDetails: (request: Request | IncomingMessage) => RequestDetails;
124
+ /**
125
+ * Wraps an Express or Node ServerResponse with a uniform API for setting headers,
126
+ * status codes, and sending JSON responses. Handles the Express vs raw Node API differences.
127
+ * @param response - Express or Node.js server response object
128
+ * @returns A ResponseDetails object with setHeader, setStatusCode, and sendResponse helpers
129
+ */
130
+ declare const getResponseDetails: (response: Response | ServerResponse) => ResponseDetails;
37
131
  //#endregion
38
- export { type ExpressMiddlewareFn, type ExpressMiddlewareHooks, type ExpressMiddlewareOptions, attachRPC, attachVite, createMiddleware, createRPCMiddleware, getRequestDetails, getResponseDetails, hasPreParsedBody, isExpressRequest, isExpressResponse, readBody };
132
+ export { type ExpressMiddlewareFn, type ExpressMiddlewareHooks, type ExpressMiddlewareOptions, type RequestDetails, type ResponseDetails, attachRPC, attachVite, createMiddleware, createRPCMiddleware, getRequestDetails, getResponseDetails, hasPreParsedBody, isExpressRequest, isExpressResponse, readBody };
39
133
  //# 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":";;;;;KAKY,2BAA2B;KAE3B,uBACV,UAAU,yCAEV,iBAAiB,QAAQ,8BACtB;UAEY;EACf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiB,UACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;cCGM,kBAAkB;cAmElB,qBAAqB;;;iBC5EZ,UAAU,KAAK,UAAO;iBAM5B,WAAW,KAAK,SAAS,MAAM;cAKlC,WAAQ,KACd,UAAiB,oBACrB,QAAQ;cAsDE,mBAAgB,KACtB,kBAAkB,YACtB,OAAO;cAIG,oBAAiB,KACvB,iBAAiB,aACrB,OAAO;cAIG,mBAAgB,KACtB,kBAAkB,YACtB,OAAO;cAIG,oBAAiB,SACnB,UAAiB;EAQxB;EACA;EACA,cAAY;EACZ,6BAAO;EACP;;cAIS,qBAAkB,UACnB,WAAkB;;EAIH,YAAA,cAAM;;EAQF,gBAAA;EAQD,eAAA,cAAM,QAAU,eAAe"}
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,eAAe;;;;;KAM1C;;EAEV;;EAEA;;EAEA,cAAc;;EAEd,SAAS;;EAET;;;;;;;;;;;cCtCW,kBAAkB;;;;;;;;cAyElB,qBAAqB;;;;;;;;iBC5FZ,UAAU,KAAK,UAAO;;;;;;iBAW5B,WAAW,KAAK,SAAS,MAAM;;;;;;;;cAWlC,WAAQ,KACd,UAAiB,oBACrB,QAAQ;;;;;;cA2DE,mBAAgB,KACtB,kBAAkB,YACtB,OAAO;;;;;;cASG,oBAAiB,KACvB,iBAAiB,aACrB,OAAO;;;;;;;cAUG,mBAAgB,KACtB,kBAAkB,YACtB,OAAO;;;;;;;cAUG,oBAAiB,SACnB,UAAiB,oBACzB;;;;;;;cAqBU,qBAAkB,UACnB,WAAkB,mBAC3B"}