@thednp/rpc 0.0.1 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -5
- package/CLAUDE.md +1 -0
- package/README.md +195 -47
- package/dist/express/express.d.mts +110 -16
- package/dist/express/express.d.mts.map +1 -1
- package/dist/express/express.mjs +113 -20
- package/dist/express/express.mjs.map +1 -1
- package/dist/fastify/fastify.d.mts +83 -5
- package/dist/fastify/fastify.d.mts.map +1 -1
- package/dist/fastify/fastify.mjs +84 -18
- package/dist/fastify/fastify.mjs.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.d.mts +64 -9
- package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.mjs +73 -18
- package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
- package/dist/helpers/helpers.d.mts +66 -4
- package/dist/helpers/helpers.d.mts.map +1 -1
- package/dist/helpers/helpers.mjs +34 -7
- package/dist/helpers/helpers.mjs.map +1 -1
- package/dist/hono/hono.d.mts +75 -6
- package/dist/hono/hono.d.mts.map +1 -1
- package/dist/hono/hono.mjs +84 -24
- package/dist/hono/hono.mjs.map +1 -1
- package/dist/index.d.mts +210 -19
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +137 -31
- package/dist/index.mjs.map +1 -1
- package/dist/koa/koa.d.mts +52 -0
- package/dist/koa/koa.d.mts.map +1 -1
- package/dist/koa/koa.mjs +86 -16
- package/dist/koa/koa.mjs.map +1 -1
- package/dist/server/server.d.mts +111 -10
- package/dist/server/server.d.mts.map +1 -1
- package/dist/server/server.mjs +117 -17
- package/dist/server/server.mjs.map +1 -1
- package/package.json +48 -31
- package/wiki/adapters.md +0 -143
- package/wiki/best-practices.md +0 -201
- package/wiki/client-usage.md +0 -62
- package/wiki/configuration.md +0 -77
- package/wiki/getting-started.md +0 -76
- package/wiki/index.md +0 -26
- package/wiki/security.md +0 -54
- package/wiki/server-functions.md +0 -93
- package/wiki/setup.md +0 -77
package/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(
|
|
101
|
-
- **Prefix regex injection prevention**: `
|
|
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`, `
|
|
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
|
-
| `
|
|
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
|
-
[](https://coveralls.io/github/thednp/rpc)
|
|
4
|
+
[](https://github.com/thednp/rpc/actions/workflows/ci.yml)
|
|
5
5
|
[](https://www.npmjs.com/package/@thednp/rpc)
|
|
6
|
+
[](https://jsr.io/@thednp/rpc)
|
|
6
7
|
[](http://npm-stat.com/charts.html?package=@thednp/rpc)
|
|
7
8
|
|
|
8
|
-
A Vite plugin for automatic RPC generation
|
|
9
|
+
A Vite plugin for automatic RPC generation — simple, framework agnostic, and easy to use.
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
* Typed client `fetch` based modules are generated and available via the RPC plugin.
|
|
11
|
+
## Isomorphic Design
|
|
12
12
|
|
|
13
|
-
The
|
|
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
|
|
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
|
-
|
|
65
|
+
<details>
|
|
66
|
+
<summary><b>Typed client modules, generated at build time</b></summary>
|
|
20
67
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
71
|
+
<details>
|
|
72
|
+
<summary><b>Cancellation should be easy</b></summary>
|
|
27
73
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
38
|
-
| -----------------|
|
|
39
|
-
| SPA - node:http | [examples/spa](https://github.com/thednp/
|
|
40
|
-
| SSR - node:http | [examples/ssr](https://github.com/thednp/
|
|
41
|
-
| Express | [examples/express](https://github.com/thednp/
|
|
42
|
-
| Fastify | [examples/fastify](https://github.com/thednp/
|
|
43
|
-
| Hono | [examples/hono](https://github.com/thednp/
|
|
44
|
-
| Koa | [examples/koa](https://github.com/thednp/
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
197
|
+
Check [Server Functions Guide](./wiki/server-functions.md) for details.
|
|
198
|
+
|
|
199
|
+
### 4. Call it in your code
|
|
116
200
|
|
|
117
|
-
Import the
|
|
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.
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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":"
|
|
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"}
|