@thednp/rpc 0.0.4 → 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/README.md +2 -0
- package/dist/fastify/fastify.d.mts +21 -8
- package/dist/fastify/fastify.d.mts.map +1 -1
- package/dist/fastify/fastify.mjs +1 -6
- package/dist/fastify/fastify.mjs.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.d.mts +8 -8
- package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.mjs +1 -6
- package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
- package/dist/helpers/helpers.d.mts +11 -10
- package/dist/helpers/helpers.d.mts.map +1 -1
- package/dist/helpers/helpers.mjs.map +1 -1
- package/dist/hono/hono.d.mts +9 -1
- package/dist/hono/hono.d.mts.map +1 -1
- package/dist/hono/hono.mjs.map +1 -1
- package/dist/index.d.mts +29 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/server/server.d.mts +17 -16
- package/dist/server/server.d.mts.map +1 -1
- package/dist/server/server.mjs.map +1 -1
- package/package.json +3 -4
- package/wiki/adapters.md +0 -172
- package/wiki/best-practices.md +0 -364
- package/wiki/client-usage.md +0 -66
- package/wiki/configuration.md +0 -76
- package/wiki/getting-started.md +0 -105
- package/wiki/index.md +0 -14
- package/wiki/security.md +0 -76
- package/wiki/server-functions.md +0 -137
- package/wiki/setup.md +0 -81
package/wiki/best-practices.md
DELETED
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
# Best Practices
|
|
2
|
-
|
|
3
|
-
## @thednp/rpc is a Transport Pipe
|
|
4
|
-
|
|
5
|
-
The best thing to do **first** is to understand `@thednp/rpc` is that it only handles **transport** and **serialization**.
|
|
6
|
-
|
|
7
|
-
It does not provide:
|
|
8
|
-
- Caching
|
|
9
|
-
- Authentication
|
|
10
|
-
- Validation
|
|
11
|
-
- State management
|
|
12
|
-
- Request limits
|
|
13
|
-
- Retry logic
|
|
14
|
-
|
|
15
|
-
Use dedicated tools for these concerns.
|
|
16
|
-
|
|
17
|
-
## Client-Side Caching
|
|
18
|
-
|
|
19
|
-
All major frameworks have a `@tanstack/<framework>-query` made by [Tanstack](https://tanstack.com/) to cover all needs except validation and authentication.
|
|
20
|
-
|
|
21
|
-
For instance the `@tanstack/react-query` is the recommended layer for React apps client-side caching, invalidation, and stale-while-revalidate:
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
// src/components/GreetUser.tsx
|
|
25
|
-
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
26
|
-
import { sayHi } from '../api';
|
|
27
|
-
|
|
28
|
-
function GreetUser({ name }: { name: string }) {
|
|
29
|
-
const { data } = useQuery({
|
|
30
|
-
queryKey: ['say-hi', name],
|
|
31
|
-
queryFn: ({ signal }) => {
|
|
32
|
-
const result = sayHi(name);
|
|
33
|
-
signal.addEventListener('abort', () => result.cancel('query cancelled'));
|
|
34
|
-
return result.data;
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
return <div>{data ?? 'Loading...'}</div>;
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## AbortSignal Best Practices
|
|
42
|
-
|
|
43
|
-
Always check `signal.aborted` or call `signal.throwIfAborted()` in long-running server functions:
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
export const processBatch = createServerFunction(
|
|
47
|
-
'process-batch',
|
|
48
|
-
async (signal: AbortSignal, items: string[]) => {
|
|
49
|
-
const results = [];
|
|
50
|
-
const errors = [];
|
|
51
|
-
for (const item of items) {
|
|
52
|
-
signal.throwIfAborted();
|
|
53
|
-
const result = await heavyWork(item);
|
|
54
|
-
results.push(result);
|
|
55
|
-
// handle errors properly
|
|
56
|
-
}
|
|
57
|
-
return results;
|
|
58
|
-
},
|
|
59
|
-
);
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Input Validation
|
|
63
|
-
|
|
64
|
-
Always validate client-provided data before using it in server functions. Use libraries like **zod** or **valibot** to parse and validate inputs:
|
|
65
|
-
|
|
66
|
-
```ts
|
|
67
|
-
// src/api/server.ts
|
|
68
|
-
import { z } from 'zod';
|
|
69
|
-
import { createServerFunction } from '@thednp/rpc/server';
|
|
70
|
-
|
|
71
|
-
const ProfileSchema = z.object({
|
|
72
|
-
name: z.string().min(1).max(100),
|
|
73
|
-
age: z.number().int().positive(),
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
export const updateProfile = createServerFunction(
|
|
77
|
-
'update-profile',
|
|
78
|
-
async (signal, raw) => {
|
|
79
|
-
const parsed = ProfileSchema.safeParse(raw);
|
|
80
|
-
if (!parsed.success) {
|
|
81
|
-
return { ok: false, errors: parsed.error.flatten().fieldErrors };
|
|
82
|
-
}
|
|
83
|
-
// parsed.data is fully typed
|
|
84
|
-
await saveToDb(parsed.data);
|
|
85
|
-
return { ok: true };
|
|
86
|
-
},
|
|
87
|
-
);
|
|
88
|
-
```
|
|
89
|
-
|
|
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.
|
|
91
|
-
|
|
92
|
-
## Authentication
|
|
93
|
-
|
|
94
|
-
> Never add auth hooks inside server functions.
|
|
95
|
-
|
|
96
|
-
Use authentication middleware **before** `createRPCMiddleware()`.
|
|
97
|
-
|
|
98
|
-
```ts
|
|
99
|
-
app.use(authMiddleware);
|
|
100
|
-
app.use(createRPCMiddleware());
|
|
101
|
-
```
|
|
102
|
-
|
|
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
|
-
```
|
|
187
|
-
|
|
188
|
-
## Body Limits
|
|
189
|
-
|
|
190
|
-
In most cases you should rely on your framework's body-parser middleware:
|
|
191
|
-
|
|
192
|
-
```ts
|
|
193
|
-
// Express
|
|
194
|
-
import express from "express";
|
|
195
|
-
const app = express();
|
|
196
|
-
app.use(express.json({ limit: 1024 * 1024 })); // or "1mb"
|
|
197
|
-
app.use(createRPCMiddleware());
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
```ts
|
|
201
|
-
// Fastify
|
|
202
|
-
import Fastify from "fastify";
|
|
203
|
-
|
|
204
|
-
const app = Fastify({ logger: false, bodyLimit: 1024 * 1024 }); // "1MB"
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
```ts
|
|
208
|
-
// Hono
|
|
209
|
-
import { Hono } from "hono";
|
|
210
|
-
import { bodyLimit } from 'hono/body-limit';
|
|
211
|
-
|
|
212
|
-
const app = new Hono();
|
|
213
|
-
app.use('*', bodyLimit({ maxSize: 1024 * 1024 })) // 1MB
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
```ts
|
|
217
|
-
// Koa
|
|
218
|
-
import Koa from "koa";
|
|
219
|
-
import { koaBody } from 'koa-body';
|
|
220
|
-
|
|
221
|
-
const app = new Koa();
|
|
222
|
-
app.use(koaBody({ jsonLimit: 1024 * 1024 })); // 1MB
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
In other cases, your custom [server app](../examples/ssr/http-express.ts) can use something like this:
|
|
226
|
-
```ts
|
|
227
|
-
// SSR (custom node:http server with Vite middleware mode)
|
|
228
|
-
import { createMiddleware, readBody } from "@thednp/rpc/express";
|
|
229
|
-
import { loadRPCConfig } from "@thednp/rpc";
|
|
230
|
-
|
|
231
|
-
const config = await loadRPCConfig();
|
|
232
|
-
const MAX_BODY_SIZE = 1024 * 1024;
|
|
233
|
-
|
|
234
|
-
app.use(createMiddleware({
|
|
235
|
-
rpcPrefix: config.rpcPrefix,
|
|
236
|
-
handler: async (req, res, next) => {
|
|
237
|
-
const { data } = await readBody(req);
|
|
238
|
-
if (Buffer.byteLength(typeof data === "string" ? data : JSON.stringify(data)) > MAX_BODY_SIZE) {
|
|
239
|
-
res.statusCode = 413;
|
|
240
|
-
res.end("Payload Too Large");
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
req.body = data;
|
|
244
|
-
next();
|
|
245
|
-
},
|
|
246
|
-
}));
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
For SPA you can make use of the vite runtime [proxy](../examples/spa/vite.config.ts)
|
|
250
|
-
```ts
|
|
251
|
-
// SPA (dedicated RPC proxy server)
|
|
252
|
-
import { readBody } from "@thednp/rpc/express";
|
|
253
|
-
|
|
254
|
-
const MAX_BODY_SIZE = 1024 * 1024;
|
|
255
|
-
|
|
256
|
-
const bodyLimit = async (req, res, next) => {
|
|
257
|
-
const { data } = await readBody(req);
|
|
258
|
-
if (Buffer.byteLength(typeof data === "string" ? data : JSON.stringify(data)) > MAX_BODY_SIZE) {
|
|
259
|
-
res.statusCode = 413;
|
|
260
|
-
res.end("Payload Too Large");
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
req.body = data;
|
|
264
|
-
next();
|
|
265
|
-
};
|
|
266
|
-
```
|
|
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
|
-
|
|
343
|
-
## SSR Guidance
|
|
344
|
-
|
|
345
|
-
- On the **server**, `createServerFunction` runs directly (not Vite-transformed).
|
|
346
|
-
- On the **client**, the plugin replaces server function calls with `fetch` based client modules.
|
|
347
|
-
- Keep `src/entry-client.ts` and `src/entry-server.ts` separate for proper hydration.
|
|
348
|
-
|
|
349
|
-
## File Naming
|
|
350
|
-
|
|
351
|
-
Only files matching `server.ts`, `server.js`, `server.mjs`, `server.mts` in `src/api/` are scanned. Export functions individually for proper client module mapping:
|
|
352
|
-
|
|
353
|
-
```ts
|
|
354
|
-
// ✅ Good — individual exports
|
|
355
|
-
export const sayHi = createServerFunction('say-hi', fn);
|
|
356
|
-
export const add = createServerFunction('add-numbers', fn);
|
|
357
|
-
|
|
358
|
-
// ❌ Avoid — default export or bundled objects
|
|
359
|
-
export default { sayHi, add };
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
## Cache is Not @thednp/rpc's Job
|
|
363
|
-
|
|
364
|
-
Use `react-query`, `SWR`, or your framework's data hooks for caching. `@thednp/rpc` is the **transport layer only**.
|
package/wiki/client-usage.md
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# Client Usage
|
|
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
|
-
|
|
7
|
-
## Auto-Generated Client Modules
|
|
8
|
-
|
|
9
|
-
When you import from `./api` in your client code, the plugin intercepts the import and generates a client module for each server function.
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
import { sayHi, add } from './api';
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Each imported function returns:
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
{ data: Promise<T>, cancel: (reason: string) => void }
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
- **`data`** — A promise that resolves to the server function's return value.
|
|
22
|
-
- **`cancel(reason: string)`** — Aborts the underlying fetch request, causing `signal.aborted` to be set in the server function.
|
|
23
|
-
|
|
24
|
-
### Example
|
|
25
|
-
|
|
26
|
-
```ts
|
|
27
|
-
import { sayHi } from './api';
|
|
28
|
-
|
|
29
|
-
const { data, cancel } = sayHi('World');
|
|
30
|
-
const result = await data; // "Hello World!"
|
|
31
|
-
cancel('user cancelled'); // triggers AbortController on the client side
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Error Handling
|
|
35
|
-
|
|
36
|
-
- **Fetch errors** (network failure, CORS) — thrown from `await data`
|
|
37
|
-
- **HTTP 4xx/5xx responses** — thrown from `await data`
|
|
38
|
-
- **Cancellation** — aborts the fetch and warns `"Request was cancelled"` in the console
|
|
39
|
-
|
|
40
|
-
## @tanstack/react-query Integration
|
|
41
|
-
|
|
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`:
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
46
|
-
import { sayHi } from './api';
|
|
47
|
-
|
|
48
|
-
function GreetUser({ name }: { name: string }) {
|
|
49
|
-
const queryClient = useQueryClient();
|
|
50
|
-
|
|
51
|
-
const { data } = useQuery({
|
|
52
|
-
queryKey: ['say-hi', name],
|
|
53
|
-
queryFn: ({ signal }) => {
|
|
54
|
-
const result = sayHi(name);
|
|
55
|
-
signal.addEventListener('abort', () => result.cancel('query cancelled'));
|
|
56
|
-
return result.data;
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
return <div>{data ?? 'Loading...'}</div>;
|
|
61
|
-
}
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Combine `cancel()` with React Query's `signal` for proper abort handling during component unmount or query invalidation.
|
|
65
|
-
|
|
66
|
-
Other frameworks have a `@tanstack/<framework>-query` made by [Tanstack](https://tanstack.com/).
|
package/wiki/configuration.md
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# Configuration
|
|
2
|
-
|
|
3
|
-
## `rpc.config.ts`
|
|
4
|
-
|
|
5
|
-
Create `rpc.config.ts` in your project root for system-wide configuration:
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
import { defineConfig } from '@thednp/rpc';
|
|
9
|
-
|
|
10
|
-
export default defineConfig({
|
|
11
|
-
rpcPrefix: '__rpc',
|
|
12
|
-
adapter: 'express',
|
|
13
|
-
});
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## `vite.config.ts`
|
|
17
|
-
|
|
18
|
-
Update your `vite.config.ts` in your project root and set additional development options:
|
|
19
|
-
|
|
20
|
-
```ts
|
|
21
|
-
import { defineConfig } from 'vite';
|
|
22
|
-
import rpc from '@thednp/rpc';
|
|
23
|
-
|
|
24
|
-
export default defineConfig({
|
|
25
|
-
plugins: [rpc(/* development options */)]
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
> **NOTE** these plugin options only apply to **development** and override the options in `rpc.config.ts`.
|
|
31
|
-
|
|
32
|
-
### Options
|
|
33
|
-
|
|
34
|
-
| Option | Type | Default | Description |
|
|
35
|
-
| --------------| ----------| -------------| --------------------------------------------------------------|
|
|
36
|
-
| `rpcPrefix` | `string` | `'__rpc'` | RPC endpoint prefix used in URL routing |
|
|
37
|
-
| `adapter` | `string` | `'express'` | Target adapter (`'express'`, `'fastify'`, `'hono'`, `'koa'`) |
|
|
38
|
-
|
|
39
|
-
## Config File Discovery
|
|
40
|
-
|
|
41
|
-
The plugin searches for config files in this order:
|
|
42
|
-
|
|
43
|
-
1. `rpc.config.ts`
|
|
44
|
-
2. `rpc.config.js`
|
|
45
|
-
3. `rpc.config.mjs`
|
|
46
|
-
4. `rpc.config.mts`
|
|
47
|
-
5. `.rpcrc.ts`
|
|
48
|
-
6. `.rpcrc.js`
|
|
49
|
-
|
|
50
|
-
The first file found is used. If none is found, defaults are applied.
|
|
51
|
-
|
|
52
|
-
## Utilities
|
|
53
|
-
|
|
54
|
-
### `defineConfig`
|
|
55
|
-
|
|
56
|
-
Type-safe helper for creating the config object. Provides autocomplete and type checking for all options.
|
|
57
|
-
|
|
58
|
-
```ts
|
|
59
|
-
import { defineConfig } from '@thednp/rpc';
|
|
60
|
-
|
|
61
|
-
export default defineConfig({
|
|
62
|
-
rpcPrefix: '__rpc',
|
|
63
|
-
});
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### `loadRPCConfig`
|
|
67
|
-
|
|
68
|
-
Programmatically load the RPC config, useful in custom server setups:
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
import { loadRPCConfig } from '@thednp/rpc';
|
|
72
|
-
|
|
73
|
-
const config = await loadRPCConfig();
|
|
74
|
-
console.log(config.rpcPrefix); // '__rpc'
|
|
75
|
-
console.log(config.adapter); // 'express'
|
|
76
|
-
```
|
package/wiki/getting-started.md
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
# Getting Started
|
|
2
|
-
|
|
3
|
-
## Installation
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
// pnpm + jsr registry
|
|
7
|
-
pnpm add jsr:@thednp/rpc
|
|
8
|
-
```
|
|
9
|
-
|
|
10
|
-
```bash
|
|
11
|
-
// pnpm + npmjs registry
|
|
12
|
-
pnpm add @thednp/rpc
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
// npm + npmjs registry
|
|
17
|
-
npm install @thednp/rpc
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
```bash
|
|
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
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Quick Start
|
|
36
|
-
|
|
37
|
-
For a quick understanding of a project setup check the [dedicated wiki section](./setup.md).
|
|
38
|
-
|
|
39
|
-
### 1. Configure system wide configuration `rpc.config.ts`
|
|
40
|
-
|
|
41
|
-
```ts
|
|
42
|
-
import { defineConfig } from "@thednp/rpc";
|
|
43
|
-
|
|
44
|
-
export default defineConfig({
|
|
45
|
-
rpcPrefix: "__rpc",
|
|
46
|
-
adapter: "express",
|
|
47
|
-
});
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Currently `@thednp/rpc` supports `'express'`, `'fastify'`, `'hono'` and `'koa'`. Check [adapters](./adapters.md) for more guides.
|
|
51
|
-
|
|
52
|
-
Also check [configuration](./configuration.md) for more guides.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
### 2. Add the plugin to `vite.config.ts`
|
|
56
|
-
|
|
57
|
-
```ts
|
|
58
|
-
import rpc from '@thednp/rpc';
|
|
59
|
-
|
|
60
|
-
export default {
|
|
61
|
-
plugins: [rpc()],
|
|
62
|
-
};
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Check [configuration](./configuration.md) for more guides.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
### 3. Create a server function in `src/api/server.ts`
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
import { createServerFunction } from '@thednp/rpc/server';
|
|
72
|
-
|
|
73
|
-
export const sayHi = createServerFunction(
|
|
74
|
-
'say-hi',
|
|
75
|
-
async (signal: AbortSignal, name: string) => {
|
|
76
|
-
signal.throwIfAborted();
|
|
77
|
-
await new Promise((res) => setTimeout(res, 1500));
|
|
78
|
-
return `Hello ${name}!`;
|
|
79
|
-
},
|
|
80
|
-
);
|
|
81
|
-
```
|
|
82
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
```ts
|
|
94
|
-
import { sayHi } from './api';
|
|
95
|
-
|
|
96
|
-
const { data, cancel } = sayHi('World');
|
|
97
|
-
const result = await data; // "Hello World!"
|
|
98
|
-
cancel('user cancelled');
|
|
99
|
-
```
|
|
100
|
-
|
|
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
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# @thednp/rpc
|
|
2
|
-
|
|
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
|
-
|
|
5
|
-
## Table of Contents
|
|
6
|
-
|
|
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
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# Security
|
|
2
|
-
|
|
3
|
-
## Prefix Boundary Check
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
```
|
|
8
|
-
rpcPrefix = '__rpc'
|
|
9
|
-
|
|
10
|
-
# Safe: matches /__rpc/foo
|
|
11
|
-
// __rpc/foo → /__rpc/
|
|
12
|
-
|
|
13
|
-
# Not matched: /__rpc-evil/foo or /foo/__rpc/bar
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
Using `startsWith` would incorrectly match paths like `/__rpc-evil/foo`, which could route to unintended handlers. The regex ensures the prefix is a standalone path segment.
|
|
17
|
-
|
|
18
|
-
## Koa URL Normalization
|
|
19
|
-
|
|
20
|
-
The Koa adapter parses `ctx.url` through `new URL()` before prefix checking. This strips query strings and normalizes encoding, preventing query-string injection attacks:
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
const url = new URL(ctx.url, 'http://localhost');
|
|
24
|
-
const pathname = url.pathname; // clean, no query string
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Generic 404 Responses
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
## Authentication via Middleware
|
|
53
|
-
|
|
54
|
-
Authentication is handled by middleware registered **before** `createRPCMiddleware()`. The middleware chain composes naturally:
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
// Express example
|
|
58
|
-
app.use(authMiddleware); // auth first
|
|
59
|
-
app.use(createRPCMiddleware()); // RPC second
|
|
60
|
-
```
|
|
61
|
-
|
|
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.
|
|
63
|
-
|
|
64
|
-
## Body Size Limits
|
|
65
|
-
|
|
66
|
-
JSON body size limits are handled by your framework's body-parser middleware:
|
|
67
|
-
|
|
68
|
-
- **Express**: `express.json({ limit: '1mb' })` (default **100kb**)
|
|
69
|
-
- **Fastify**: `bodyLimit` option in Fastify config (default **1 MiB**)
|
|
70
|
-
- **Koa**: `koa-body({ formLimit: '1mb' })`
|
|
71
|
-
- **Hono**: Built-in body size limiting
|
|
72
|
-
|
|
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.
|
|
74
|
-
|
|
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.
|