@thednp/rpc 0.0.1 → 0.0.4
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 +193 -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 +66 -1
- package/dist/fastify/fastify.d.mts.map +1 -1
- package/dist/fastify/fastify.mjs +89 -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 +78 -18
- package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
- package/dist/helpers/helpers.d.mts +64 -3
- 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 +66 -5
- 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 +182 -18
- 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 +104 -4
- 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 -30
- package/wiki/adapters.md +31 -2
- package/wiki/best-practices.md +172 -9
- package/wiki/client-usage.md +7 -3
- package/wiki/configuration.md +5 -6
- package/wiki/getting-started.md +35 -6
- package/wiki/index.md +8 -20
- package/wiki/security.md +27 -5
- package/wiki/server-functions.md +49 -5
- package/wiki/setup.md +8 -4
package/wiki/best-practices.md
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## @thednp/rpc is a Transport Pipe
|
|
4
4
|
|
|
5
|
-
`@thednp/rpc`
|
|
5
|
+
The best thing to do **first** is to understand `@thednp/rpc` is that it only handles **transport** and **serialization**.
|
|
6
6
|
|
|
7
|
+
It does not provide:
|
|
7
8
|
- Caching
|
|
8
9
|
- Authentication
|
|
9
10
|
- Validation
|
|
10
11
|
- State management
|
|
12
|
+
- Request limits
|
|
11
13
|
- Retry logic
|
|
12
14
|
|
|
13
15
|
Use dedicated tools for these concerns.
|
|
@@ -85,23 +87,107 @@ export const updateProfile = createServerFunction(
|
|
|
85
87
|
);
|
|
86
88
|
```
|
|
87
89
|
|
|
88
|
-
Return validation errors as structured data — the client's `handleResponse` will surface them as an `Error`.
|
|
90
|
+
Return validation errors as structured data — the client's `handleResponse` will surface them as an `Error`. Check [Server Functions Guide](./server-functions.md) for more detailed examples.
|
|
89
91
|
|
|
90
92
|
## Authentication
|
|
91
93
|
|
|
92
|
-
|
|
94
|
+
> Never add auth hooks inside server functions.
|
|
95
|
+
|
|
96
|
+
Use authentication middleware **before** `createRPCMiddleware()`.
|
|
93
97
|
|
|
94
98
|
```ts
|
|
95
|
-
// Express
|
|
96
99
|
app.use(authMiddleware);
|
|
97
100
|
app.use(createRPCMiddleware());
|
|
98
101
|
```
|
|
99
102
|
|
|
100
|
-
|
|
103
|
+
|
|
104
|
+
### Basic Authorization
|
|
105
|
+
|
|
106
|
+
A typical auth middleware reads credentials from the request, validates them, and sets `req.user`:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// Express
|
|
110
|
+
const authMiddleware = (req, res, next) => {
|
|
111
|
+
const token = req.headers.authorization?.replace("Bearer ", "");
|
|
112
|
+
if (!token) {
|
|
113
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
req.user = verifyToken(token); // { id, role, ... }
|
|
118
|
+
next();
|
|
119
|
+
} catch {
|
|
120
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
> In your production apps, **use best solutions** provided by server of your choice. The above code is only to showcase how to wire pieces together.
|
|
126
|
+
|
|
127
|
+
### Per-Function Authorization
|
|
128
|
+
|
|
129
|
+
When some server functions should be public and others require authentication, use `createMiddleware` with a handler that inspects the function name:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import type { Request } from "express";
|
|
133
|
+
import { createMiddleware } from "@thednp/rpc/express";
|
|
134
|
+
import { loadRPCConfig } from "@thednp/rpc";
|
|
135
|
+
|
|
136
|
+
const publicFns = new Set(["login", "register", "publicData"]);
|
|
137
|
+
const config = await loadRPCConfig();
|
|
138
|
+
|
|
139
|
+
// use a type to better describe your requests
|
|
140
|
+
type UserRequest = Request & {
|
|
141
|
+
user: YourUserType
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const rpcAuthz = createMiddleware({
|
|
145
|
+
rpcPrefix: config.rpcPrefix,
|
|
146
|
+
handler: async (req: UserRequest, res, next) => {
|
|
147
|
+
const url = new URL(req.url, "http://localhost").pathname;
|
|
148
|
+
const fnName = url.replace(config.rpcPrefix, "");
|
|
149
|
+
const user = req.user; // set by earlier auth middleware
|
|
150
|
+
|
|
151
|
+
// Allow public functions through
|
|
152
|
+
if (publicFns.has(fnName)) return next();
|
|
153
|
+
|
|
154
|
+
// Reject unauthenticated
|
|
155
|
+
if (!user) {
|
|
156
|
+
res.statusCode = 401;
|
|
157
|
+
res.setHeader("Content-Type", "application/json");
|
|
158
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
next();
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Wire it between auth and the RPC handler:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
app.use(authMiddleware); // sets req.user
|
|
171
|
+
app.use(rpcAuthz); // per-function guard
|
|
172
|
+
app.use(createRPCMiddleware());
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
For role-based access, extend with a map:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const roleAccess: Record<string, string[]> = {
|
|
179
|
+
deleteUser: ["admin"],
|
|
180
|
+
updateProfile: ["user", "admin"],
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
if (!user || !roleAccess[fnName]?.includes(user.role)) {
|
|
184
|
+
// 401 or 403
|
|
185
|
+
}
|
|
186
|
+
```
|
|
101
187
|
|
|
102
188
|
## Body Limits
|
|
103
189
|
|
|
104
|
-
In most cases you
|
|
190
|
+
In most cases you should rely on your framework's body-parser middleware:
|
|
105
191
|
|
|
106
192
|
```ts
|
|
107
193
|
// Express
|
|
@@ -136,8 +222,9 @@ const app = new Koa();
|
|
|
136
222
|
app.use(koaBody({ jsonLimit: 1024 * 1024 })); // 1MB
|
|
137
223
|
```
|
|
138
224
|
|
|
225
|
+
In other cases, your custom [server app](../examples/ssr/http-express.ts) can use something like this:
|
|
139
226
|
```ts
|
|
140
|
-
// SSR (custom http server with Vite middleware mode)
|
|
227
|
+
// SSR (custom node:http server with Vite middleware mode)
|
|
141
228
|
import { createMiddleware, readBody } from "@thednp/rpc/express";
|
|
142
229
|
import { loadRPCConfig } from "@thednp/rpc";
|
|
143
230
|
|
|
@@ -145,7 +232,7 @@ const config = await loadRPCConfig();
|
|
|
145
232
|
const MAX_BODY_SIZE = 1024 * 1024;
|
|
146
233
|
|
|
147
234
|
app.use(createMiddleware({
|
|
148
|
-
|
|
235
|
+
rpcPrefix: config.rpcPrefix,
|
|
149
236
|
handler: async (req, res, next) => {
|
|
150
237
|
const { data } = await readBody(req);
|
|
151
238
|
if (Buffer.byteLength(typeof data === "string" ? data : JSON.stringify(data)) > MAX_BODY_SIZE) {
|
|
@@ -159,6 +246,7 @@ app.use(createMiddleware({
|
|
|
159
246
|
}));
|
|
160
247
|
```
|
|
161
248
|
|
|
249
|
+
For SPA you can make use of the vite runtime [proxy](../examples/spa/vite.config.ts)
|
|
162
250
|
```ts
|
|
163
251
|
// SPA (dedicated RPC proxy server)
|
|
164
252
|
import { readBody } from "@thednp/rpc/express";
|
|
@@ -177,10 +265,85 @@ const bodyLimit = async (req, res, next) => {
|
|
|
177
265
|
};
|
|
178
266
|
```
|
|
179
267
|
|
|
268
|
+
## Rate Limiting
|
|
269
|
+
|
|
270
|
+
Throttle RPC endpoints like any other route — rate limiting is the host's responsibility:
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
// Express
|
|
274
|
+
import { rateLimit } from 'express-rate-limit';
|
|
275
|
+
|
|
276
|
+
app.use('/__rpc', rateLimit({
|
|
277
|
+
windowMs: 60 * 1000, // 1 minute
|
|
278
|
+
limit: 60, // 60 requests per window
|
|
279
|
+
standardHeaders: 'draft-8',
|
|
280
|
+
legacyHeaders: false,
|
|
281
|
+
}));
|
|
282
|
+
app.use(createRPCMiddleware());
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
// Fastify
|
|
287
|
+
import rateLimit from '@fastify/rate-limit';
|
|
288
|
+
|
|
289
|
+
await app.register(rateLimit, { max: 60, timeWindow: '1 minute' });
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
// Hono
|
|
294
|
+
import { rateLimiter } from 'hono-rate-limiter';
|
|
295
|
+
|
|
296
|
+
app.use('/__rpc/*', rateLimiter({ windowMs: 60_000, limit: 60 }));
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
// Koa
|
|
301
|
+
import { rateLimit } from 'koa2-ratelimit';
|
|
302
|
+
|
|
303
|
+
app.use(rateLimit.middleware({ interval: { min: 1 }, max: 60 }));
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Origin / CSRF Protection
|
|
307
|
+
|
|
308
|
+
`@thednp/rpc` performs **no origin validation by default** — like authentication, it is opt-in so the host decides.
|
|
309
|
+
|
|
310
|
+
### Option A: `origin` middleware option
|
|
311
|
+
|
|
312
|
+
When your RPC endpoints sit behind a reverse proxy with multiple public origins (e.g. a sibling subdomain), pass the allowed origin to `createRPCMiddleware()`:
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
app.use(createRPCMiddleware({
|
|
316
|
+
origin: 'https://app.example.com',
|
|
317
|
+
}));
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Requests carrying an `Origin` header that does not match are rejected with `403 Forbidden`. Requests without an `Origin` header (curl, native apps) pass through unchecked.
|
|
321
|
+
|
|
322
|
+
> `SameSite=Lax` cookies already block cross-site `POST` from HTML forms; the `origin` option closes the remaining "sibling subdomain" case.
|
|
323
|
+
|
|
324
|
+
### Option B: custom middleware
|
|
325
|
+
|
|
326
|
+
Check `Sec-Fetch-Site` (Fetch Metadata) or `Origin` yourself before the RPC middleware:
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
// Express
|
|
330
|
+
app.use((req, res, next) => {
|
|
331
|
+
const site = req.headers['sec-fetch-site'];
|
|
332
|
+
if (site && site !== 'same-origin' && site !== 'none') {
|
|
333
|
+
res.status(403).json({ error: 'Forbidden' });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
next();
|
|
337
|
+
});
|
|
338
|
+
app.use(createRPCMiddleware());
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Note that strict checks rejecting requests *without* these headers (TanStack Start style) will break curl and native clients — only enforce when the header is present, or provide an opt-out.
|
|
342
|
+
|
|
180
343
|
## SSR Guidance
|
|
181
344
|
|
|
182
345
|
- On the **server**, `createServerFunction` runs directly (not Vite-transformed).
|
|
183
|
-
- On the **client**, the plugin replaces server function calls with `fetch
|
|
346
|
+
- On the **client**, the plugin replaces server function calls with `fetch` based client modules.
|
|
184
347
|
- Keep `src/entry-client.ts` and `src/entry-server.ts` separate for proper hydration.
|
|
185
348
|
|
|
186
349
|
## File Naming
|
package/wiki/client-usage.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Client Usage
|
|
2
2
|
|
|
3
|
+
Server functions, despite their name, work in both server and client side (transformed into `fetch` based modules by our plugin), a perfect fit for isomorphic rendering.
|
|
4
|
+
|
|
5
|
+
In most apps you will be working with client focused apps.
|
|
6
|
+
|
|
3
7
|
## Auto-Generated Client Modules
|
|
4
8
|
|
|
5
9
|
When you import from `./api` in your client code, the plugin intercepts the import and generates a client module for each server function.
|
|
@@ -15,7 +19,7 @@ Each imported function returns:
|
|
|
15
19
|
```
|
|
16
20
|
|
|
17
21
|
- **`data`** — A promise that resolves to the server function's return value.
|
|
18
|
-
- **`cancel(reason)`** — Aborts the underlying fetch request, causing `signal.aborted` to be set in the server function.
|
|
22
|
+
- **`cancel(reason: string)`** — Aborts the underlying fetch request, causing `signal.aborted` to be set in the server function.
|
|
19
23
|
|
|
20
24
|
### Example
|
|
21
25
|
|
|
@@ -31,11 +35,11 @@ cancel('user cancelled'); // triggers AbortController on the client side
|
|
|
31
35
|
|
|
32
36
|
- **Fetch errors** (network failure, CORS) — thrown from `await data`
|
|
33
37
|
- **HTTP 4xx/5xx responses** — thrown from `await data`
|
|
34
|
-
- **Cancellation** —
|
|
38
|
+
- **Cancellation** — aborts the fetch and warns `"Request was cancelled"` in the console
|
|
35
39
|
|
|
36
40
|
## @tanstack/react-query Integration
|
|
37
41
|
|
|
38
|
-
`@thednp/rpc` is a
|
|
42
|
+
`@thednp/rpc` is a transport pipe — it handles serialization and transport only. For client-side caching, data invalidation, and stale-while-revalidate patterns, use `@tanstack/react-query`:
|
|
39
43
|
|
|
40
44
|
```ts
|
|
41
45
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
package/wiki/configuration.md
CHANGED
|
@@ -8,7 +8,7 @@ Create `rpc.config.ts` in your project root for system-wide configuration:
|
|
|
8
8
|
import { defineConfig } from '@thednp/rpc';
|
|
9
9
|
|
|
10
10
|
export default defineConfig({
|
|
11
|
-
|
|
11
|
+
rpcPrefix: '__rpc',
|
|
12
12
|
adapter: 'express',
|
|
13
13
|
});
|
|
14
14
|
```
|
|
@@ -17,7 +17,6 @@ export default defineConfig({
|
|
|
17
17
|
|
|
18
18
|
Update your `vite.config.ts` in your project root and set additional development options:
|
|
19
19
|
|
|
20
|
-
|
|
21
20
|
```ts
|
|
22
21
|
import { defineConfig } from 'vite';
|
|
23
22
|
import rpc from '@thednp/rpc';
|
|
@@ -34,12 +33,12 @@ export default defineConfig({
|
|
|
34
33
|
|
|
35
34
|
| Option | Type | Default | Description |
|
|
36
35
|
| --------------| ----------| -------------| --------------------------------------------------------------|
|
|
37
|
-
| `
|
|
36
|
+
| `rpcPrefix` | `string` | `'__rpc'` | RPC endpoint prefix used in URL routing |
|
|
38
37
|
| `adapter` | `string` | `'express'` | Target adapter (`'express'`, `'fastify'`, `'hono'`, `'koa'`) |
|
|
39
38
|
|
|
40
39
|
## Config File Discovery
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
The plugin searches for config files in this order:
|
|
43
42
|
|
|
44
43
|
1. `rpc.config.ts`
|
|
45
44
|
2. `rpc.config.js`
|
|
@@ -60,7 +59,7 @@ Type-safe helper for creating the config object. Provides autocomplete and type
|
|
|
60
59
|
import { defineConfig } from '@thednp/rpc';
|
|
61
60
|
|
|
62
61
|
export default defineConfig({
|
|
63
|
-
|
|
62
|
+
rpcPrefix: '__rpc',
|
|
64
63
|
});
|
|
65
64
|
```
|
|
66
65
|
|
|
@@ -72,6 +71,6 @@ Programmatically load the RPC config, useful in custom server setups:
|
|
|
72
71
|
import { loadRPCConfig } from '@thednp/rpc';
|
|
73
72
|
|
|
74
73
|
const config = await loadRPCConfig();
|
|
75
|
-
console.log(config.
|
|
74
|
+
console.log(config.rpcPrefix); // '__rpc'
|
|
76
75
|
console.log(config.adapter); // 'express'
|
|
77
76
|
```
|
package/wiki/getting-started.md
CHANGED
|
@@ -3,35 +3,52 @@
|
|
|
3
3
|
## Installation
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
pnpm
|
|
6
|
+
// pnpm + jsr registry
|
|
7
|
+
pnpm add jsr:@thednp/rpc
|
|
7
8
|
```
|
|
8
9
|
|
|
9
10
|
```bash
|
|
10
|
-
|
|
11
|
+
// pnpm + npmjs registry
|
|
12
|
+
pnpm add @thednp/rpc
|
|
11
13
|
```
|
|
12
14
|
|
|
13
15
|
```bash
|
|
14
|
-
|
|
16
|
+
// npm + npmjs registry
|
|
17
|
+
npm install @thednp/rpc
|
|
15
18
|
```
|
|
16
19
|
|
|
17
20
|
```bash
|
|
18
|
-
|
|
21
|
+
// bun + npmjs registry
|
|
22
|
+
bun add @thednp/rpc
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
// deno + npmjs registry
|
|
27
|
+
deno add npm:@thednp/rpc
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
// deno + jsr registry
|
|
32
|
+
deno add jsr:@thednp/rpc
|
|
19
33
|
```
|
|
20
34
|
|
|
21
35
|
## Quick Start
|
|
22
36
|
|
|
37
|
+
For a quick understanding of a project setup check the [dedicated wiki section](./setup.md).
|
|
38
|
+
|
|
23
39
|
### 1. Configure system wide configuration `rpc.config.ts`
|
|
24
40
|
|
|
25
41
|
```ts
|
|
26
42
|
import { defineConfig } from "@thednp/rpc";
|
|
27
43
|
|
|
28
44
|
export default defineConfig({
|
|
29
|
-
|
|
45
|
+
rpcPrefix: "__rpc",
|
|
30
46
|
adapter: "express",
|
|
31
47
|
});
|
|
32
48
|
```
|
|
33
49
|
|
|
34
50
|
Currently `@thednp/rpc` supports `'express'`, `'fastify'`, `'hono'` and `'koa'`. Check [adapters](./adapters.md) for more guides.
|
|
51
|
+
|
|
35
52
|
Also check [configuration](./configuration.md) for more guides.
|
|
36
53
|
|
|
37
54
|
|
|
@@ -63,7 +80,15 @@ export const sayHi = createServerFunction(
|
|
|
63
80
|
);
|
|
64
81
|
```
|
|
65
82
|
|
|
66
|
-
|
|
83
|
+
Expose it in `src/api/index.ts`
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
export * from "./server"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Check the [server functions guide](./server-functions.md) for details.
|
|
90
|
+
|
|
91
|
+
### 4. Use it in your code
|
|
67
92
|
|
|
68
93
|
```ts
|
|
69
94
|
import { sayHi } from './api';
|
|
@@ -74,3 +99,7 @@ cancel('user cancelled');
|
|
|
74
99
|
```
|
|
75
100
|
|
|
76
101
|
That's it — the plugin auto-scans `src/api/server.ts`, maps exports to client functions, and replaces `./api` imports with fetch-based client modules during the Vite transform.
|
|
102
|
+
|
|
103
|
+
For a more detailed guide on client-side usage, check the [dedicated wiki section](./client-usage.md).
|
|
104
|
+
|
|
105
|
+
Next you need to [connect the adapter](./adapters.md) with your server of choice.
|
package/wiki/index.md
CHANGED
|
@@ -2,25 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
A Vite plugin for creating server functions with automatic Remote Procedure Calls (RPC) generation. Server functions are defined in a dedicated file, auto-scanned, and transformed into client-side fetch modules — no manual API route setup required.
|
|
4
4
|
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- File-level server code isolation without directives like `'use server'`
|
|
8
|
-
- System-wide configuration via `rpc.config.ts`
|
|
9
|
-
- Automatic RPC generation for server functions
|
|
10
|
-
- `AbortController` support for request cancellation
|
|
11
|
-
- Adapters for Express, Fastify, Hono, and Koa with unified API
|
|
12
|
-
- Framework-agnostic core
|
|
13
|
-
- TypeScript support
|
|
14
|
-
- Path-segment prefix matching prevents URL boundary bypass
|
|
15
|
-
- @tanstack/react-query integration for client-side caching and invalidation
|
|
16
|
-
|
|
17
5
|
## Table of Contents
|
|
18
6
|
|
|
19
|
-
- [Getting Started](getting-started.md) — Installation and quick start
|
|
20
|
-
- [Setup Guide](setup.md) — Project structure and configuration
|
|
21
|
-
- [Configuration](configuration.md) — Configuration reference
|
|
22
|
-
- [Server Functions](server-functions.md) — Creating server functions
|
|
23
|
-
- [Client Usage](client-usage.md) — Client-side usage
|
|
24
|
-
- [Adapters](adapters.md) — Framework adapters
|
|
25
|
-
- [Security](security.md) — Security hardening
|
|
26
|
-
- [Best Practices](best-practices.md) — Tips and best practices
|
|
7
|
+
- [Getting Started](./getting-started.md) — Installation and quick start
|
|
8
|
+
- [Setup Guide](./setup.md) — Project structure and configuration
|
|
9
|
+
- [Configuration](./configuration.md) — Configuration reference
|
|
10
|
+
- [Server Functions](./server-functions.md) — Creating server functions
|
|
11
|
+
- [Client Usage](./client-usage.md) — Client-side usage
|
|
12
|
+
- [Adapters](./adapters.md) — Framework adapters
|
|
13
|
+
- [Security](./security.md) — Security hardening
|
|
14
|
+
- [Best Practices](./best-practices.md) — Tips and best practices
|
package/wiki/security.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## Prefix Boundary Check
|
|
4
4
|
|
|
5
|
-
All adapters use `new RegExp(\`^/${
|
|
5
|
+
All adapters use `new RegExp(\`^/${escapeRegExp(rpcPrefix)}/\`)` instead of `startsWith` to match the RPC endpoint path. The prefix is escaped with `escapeRegExp()` before being embedded in the boundary regex, preventing ReDoS or unintended matching from metacharacters in the prefix. This prevents path-segment bypass attacks:
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
|
|
8
|
+
rpcPrefix = '__rpc'
|
|
9
9
|
|
|
10
10
|
# Safe: matches /__rpc/foo
|
|
11
11
|
// __rpc/foo → /__rpc/
|
|
@@ -28,6 +28,27 @@ const pathname = url.pathname; // clean, no query string
|
|
|
28
28
|
|
|
29
29
|
Error responses do not echo the requested function name. This prevents function enumeration — an attacker cannot discover available RPC functions by probing for non-existent endpoints.
|
|
30
30
|
|
|
31
|
+
## HTTP Method Enforcement
|
|
32
|
+
|
|
33
|
+
Server functions default to `POST`, and the middleware rejects any request whose HTTP method does not match the function's configured method with `405 Method Not Allowed`:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
GET /__rpc/do-stuff → 405 (function defaults to POST)
|
|
37
|
+
POST /__rpc/do-stuff → 200
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This blocks the simplest CSRF vector: an attacker page embedding `<img src="/__rpc/do-stuff">` or a form `GET` that would otherwise trigger side effects. Functions that opt into `method: "GET"` (via `createServerFunction(name, handler, { method: 'GET' })`) receive their arguments as an `?args=` JSON query parameter. Reserve `GET` for side-effect-free functions only. See [Server Functions Guide](./server-functions.md) for details.
|
|
41
|
+
|
|
42
|
+
## Origin Validation
|
|
43
|
+
|
|
44
|
+
`@thednp/rpc` performs no origin validation by default, but `createRPCMiddleware()` accepts an `origin` option:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
app.use(createRPCMiddleware({ origin: 'https://app.example.com' }));
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
When set, any request carrying an `Origin` header that does not match the configured origin is rejected with `403 Forbidden`. Requests **without** an `Origin` header (curl, native clients) pass through — the check only rejects when the browser-provided header disagrees. This closes the "sibling subdomain" CSRF gap that `SameSite=Lax` cookies alone cannot cover. See [Best Practices Guide](./best-practices.md) for custom middleware alternatives.
|
|
51
|
+
|
|
31
52
|
## Authentication via Middleware
|
|
32
53
|
|
|
33
54
|
Authentication is handled by middleware registered **before** `createRPCMiddleware()`. The middleware chain composes naturally:
|
|
@@ -38,7 +59,7 @@ app.use(authMiddleware); // auth first
|
|
|
38
59
|
app.use(createRPCMiddleware()); // RPC second
|
|
39
60
|
```
|
|
40
61
|
|
|
41
|
-
Do not add auth hooks inside the plugin. Use your framework's standard middleware pattern.
|
|
62
|
+
Do not add auth hooks inside the plugin. Use your framework's standard middleware pattern. Check [Best Practices Guide](./best-practices.md) for more detailed examples.
|
|
42
63
|
|
|
43
64
|
## Body Size Limits
|
|
44
65
|
|
|
@@ -49,6 +70,7 @@ JSON body size limits are handled by your framework's body-parser middleware:
|
|
|
49
70
|
- **Koa**: `koa-body({ formLimit: '1mb' })`
|
|
50
71
|
- **Hono**: Built-in body size limiting
|
|
51
72
|
|
|
52
|
-
|
|
73
|
+
The `readBody` utility of each adapter reads the raw request stream and does **not** impose a built-in size limit — always register your framework's body-parser middleware (or a custom limit handler) before `createRPCMiddleware()`. Check [Best Practices Guide](./best-practices.md) for body-limit examples.
|
|
53
74
|
|
|
54
|
-
|
|
75
|
+
## Input Validation
|
|
76
|
+
Server functions receive raw, untrusted client data. Always validate before use. Check [Server Functions Guide](./server-functions.md) for more detailed examples.
|
package/wiki/server-functions.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Server Functions
|
|
2
2
|
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Server functions run exclusively on the server. They have access to server-only resources (databases, file system, environment variables, private APIs) and **never execute on the client**.
|
|
6
|
+
|
|
7
|
+
The `@thednp/rpc` Vite plugin transforms imports of server functions into client-side stubs that call the real implementation over HTTP. This isomorphic bridge means you write your functions once and call them from either server-rendered pages or client-side code — the RPC middleware handles routing on the server while the generated client modules handle serialization, transport, and cancellation.
|
|
8
|
+
|
|
9
|
+
All examples except the SPA use SSR to demonstrate this: the same server functions are imported directly during server-side rendering (in `entry-server.ts`) and also called from client-side JavaScript (via the auto-generated fetch module).
|
|
10
|
+
|
|
11
|
+
The [SPA example](../examples/spa) uses a thin `node:http` based proxy that executes the server functions.
|
|
12
|
+
|
|
3
13
|
## `createServerFunction(name, handler, options?)`
|
|
4
14
|
|
|
5
15
|
The core API for defining server-side functions.
|
|
@@ -10,15 +20,49 @@ The core API for defining server-side functions.
|
|
|
10
20
|
function createServerFunction<T>(
|
|
11
21
|
name: string,
|
|
12
22
|
handler: (signal: AbortSignal, ...args: JsonArray) => Promise<T>,
|
|
13
|
-
options?: {
|
|
23
|
+
options?: {
|
|
24
|
+
contentType?: 'application/json' | 'text/plain',
|
|
25
|
+
credentials?: "same-origin" | "include" | "omit",
|
|
26
|
+
method?: "GET" | "POST",
|
|
27
|
+
}
|
|
14
28
|
): ServerFunction<T>;
|
|
15
29
|
```
|
|
16
30
|
|
|
17
31
|
### Parameters
|
|
18
32
|
|
|
19
|
-
- **`name`** (`string`) — The registered name used in RPC routing.
|
|
33
|
+
- **`name`** (`string`) — The registered name used in RPC routing.
|
|
20
34
|
- **`handler`** (`(signal: AbortSignal, ...args: JsonArray) => Promise<T>`) — The actual implementation. The first argument is always an `AbortSignal`; remaining arguments come from the client. The return value must be JSON-serializable.
|
|
21
|
-
- **`options`**
|
|
35
|
+
- **`options`** — Optional credentials, serialization strategy, and HTTP method
|
|
36
|
+
* `contentType?: 'application/json' | 'text/plain'` - Defaults to `'application/json'`.
|
|
37
|
+
* `credentials?: "include" | "same-origin" | "omit"` - Defaults to `'same-origin'`.
|
|
38
|
+
* `method?: "GET" | "POST"` - Defaults to `'POST'`.
|
|
39
|
+
|
|
40
|
+
### HTTP Method
|
|
41
|
+
|
|
42
|
+
By default every server function is invoked via `POST`. Functions that are safe to call from a browser URL bar, a `<script>` tag, or a CDN can opt into `GET` — arguments then travel as an `?args=` JSON query parameter:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
export const publicData = createServerFunction(
|
|
46
|
+
'public-data',
|
|
47
|
+
async (signal, topic: string) => {
|
|
48
|
+
return await fetchPublicData(topic);
|
|
49
|
+
},
|
|
50
|
+
{ method: 'GET' },
|
|
51
|
+
);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The generated client module issues a `GET /__rpc/public-data?args=%5B%22news%22%5D` request. The middleware rejects requests whose HTTP method does not match the function's configured method with `405 Method Not Allowed` — so `POST`-only functions are safe from cross-site `GET` requests, and `GET` functions can be linked/bookmarked directly.
|
|
55
|
+
|
|
56
|
+
> **Security note:** defaulting to `POST` prevents CSRF via `<img>`/`<script>`/form `GET` requests. Only set `method: "GET"` for functions with no side effects.
|
|
57
|
+
|
|
58
|
+
> **Why only `GET` and `POST`?** This is deliberate, not an oversight:
|
|
59
|
+
>
|
|
60
|
+
> - RPC dispatch is not REST — functions have no resource semantics, so the meanings of `PUT` (idempotent replace), `PATCH` (partial update), or `DELETE` (removal) don't apply to a function call. The only transport distinctions that matter are `POST` (args in the body, any payload) and `GET` (args in the query string, cacheable by browsers and CDNs).
|
|
61
|
+
> - `OPTIONS` is reserved by the HTTP protocol for CORS preflight; browsers send it automatically, and frameworks handle it. Exposing it as a function method would collide with framework CORS handling.
|
|
62
|
+
> - `HEAD` is derived from `GET` at the HTTP layer, so it needs no function-level support.
|
|
63
|
+
> - Every accepted method is another dispatch path to validate. Keeping the surface minimal (and defaulting to `POST`) reduces CSRF and parsing attack surface.
|
|
64
|
+
>
|
|
65
|
+
> If a concrete need arises (e.g. a REST-style wrapper wanting true `PUT` semantics), the `method` union is a one-line extension — adapters already centralize dispatch on it.
|
|
22
66
|
|
|
23
67
|
### AbortSignal
|
|
24
68
|
|
|
@@ -44,11 +88,11 @@ When `createServerFunction` is called, it registers the function in a server-sid
|
|
|
44
88
|
|
|
45
89
|
### Return Type
|
|
46
90
|
|
|
47
|
-
The return value of `handler` is serialized to JSON and sent as the HTTP response body. Ensure your return type is JSON-serializable
|
|
91
|
+
The return value of `handler` is serialized to JSON and sent as the HTTP response body. **Ensure your return type is JSON-serializable.**
|
|
48
92
|
|
|
49
93
|
## Input Validation
|
|
50
94
|
|
|
51
|
-
Server functions receive raw, untrusted client data. Always validate before use
|
|
95
|
+
Server functions receive raw, untrusted client data. **Always validate data within your server functions before use.**
|
|
52
96
|
|
|
53
97
|
**zod:**
|
|
54
98
|
|
package/wiki/setup.md
CHANGED
|
@@ -6,17 +6,21 @@
|
|
|
6
6
|
project/
|
|
7
7
|
├── src/
|
|
8
8
|
│ ├── api/
|
|
9
|
+
│ │ └── index.ts # All server functions exports
|
|
9
10
|
│ │ └── server.ts # Auto-scanned server functions
|
|
10
11
|
│ ├── entry-client.ts # Client entry (SSR projects)
|
|
11
12
|
│ └── entry-server.ts # Server entry (SSR projects)
|
|
12
13
|
├── vite.config.ts # Add rpc() plugin here
|
|
13
14
|
├── rpc.config.ts # Optional config
|
|
14
|
-
|
|
15
|
+
├── package.json # The project npm configuration
|
|
16
|
+
└── server.js # Your Express/Hono/Fastify/Koa server
|
|
15
17
|
```
|
|
16
18
|
|
|
19
|
+
> Various frameworks like `@tanstack-start`, `@sveltejs/kit` prefer a more specific structure, so be sure to check their documentation; most frameworks have their own data transport layer.
|
|
20
|
+
|
|
17
21
|
## Server Files
|
|
18
22
|
|
|
19
|
-
The plugin looks in `src/api/` for files
|
|
23
|
+
The plugin looks in `src/api/` for files with these **exact** names (matching is case-sensitive and non-partial, so `server.tsx`, `my-server.ts`, or `server.txt` are ignored):
|
|
20
24
|
|
|
21
25
|
- `server.ts`
|
|
22
26
|
- `server.js`
|
|
@@ -36,7 +40,7 @@ Each matched file is loaded with `vite.ssrLoadModule`, and all named exports are
|
|
|
36
40
|
|
|
37
41
|
### SSR Projects
|
|
38
42
|
|
|
39
|
-
Create both `src/entry-client.ts` and `src/entry-server.ts`. The client
|
|
43
|
+
Create both `src/entry-client.ts` and `src/entry-server.ts`. The client and server bundles both import from `./api`. On the client your server function gets transformed by the plugin. On the server, `createServerFunction` runs directly (not transformed).
|
|
40
44
|
|
|
41
45
|
### SPA Projects
|
|
42
46
|
|
|
@@ -74,4 +78,4 @@ await attachRPC(app); // production
|
|
|
74
78
|
attachVite(app, vite); // development
|
|
75
79
|
```
|
|
76
80
|
|
|
77
|
-
See [Adapters](adapters.md) for full server setup examples.
|
|
81
|
+
See [Adapters](./adapters.md) for full server setup examples.
|