@thednp/rpc 0.0.1
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 +142 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/express/express.d.mts +39 -0
- package/dist/express/express.d.mts.map +1 -0
- package/dist/express/express.mjs +179 -0
- package/dist/express/express.mjs.map +1 -0
- package/dist/fastify/fastify.d.mts +45 -0
- package/dist/fastify/fastify.d.mts.map +1 -0
- package/dist/fastify/fastify.mjs +164 -0
- package/dist/fastify/fastify.mjs.map +1 -0
- package/dist/fastify/plugin/fastify/plugin.d.mts +114 -0
- package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -0
- package/dist/fastify/plugin/fastify/plugin.mjs +151 -0
- package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -0
- package/dist/helpers/helpers.d.mts +9 -0
- package/dist/helpers/helpers.d.mts.map +1 -0
- package/dist/helpers/helpers.mjs +36 -0
- package/dist/helpers/helpers.mjs.map +1 -0
- package/dist/hono/hono.d.mts +49 -0
- package/dist/hono/hono.d.mts.map +1 -0
- package/dist/hono/hono.mjs +153 -0
- package/dist/hono/hono.mjs.map +1 -0
- package/dist/index.d.mts +192 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +296 -0
- package/dist/index.mjs.map +1 -0
- package/dist/koa/koa.d.mts +26 -0
- package/dist/koa/koa.d.mts.map +1 -0
- package/dist/koa/koa.mjs +155 -0
- package/dist/koa/koa.mjs.map +1 -0
- package/dist/server/server.d.mts +92 -0
- package/dist/server/server.d.mts.map +1 -0
- package/dist/server/server.mjs +161 -0
- package/dist/server/server.mjs.map +1 -0
- package/package.json +78 -0
- package/wiki/adapters.md +143 -0
- package/wiki/best-practices.md +201 -0
- package/wiki/client-usage.md +62 -0
- package/wiki/configuration.md +77 -0
- package/wiki/getting-started.md +76 -0
- package/wiki/index.md +26 -0
- package/wiki/security.md +54 -0
- package/wiki/server-functions.md +93 -0
- package/wiki/setup.md +77 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# @thednp/rpc
|
|
2
|
+
|
|
3
|
+
## Dev Commands
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm dev # Run examples/spa dev server
|
|
7
|
+
pnpm dev:express # Run examples/express dev server
|
|
8
|
+
pnpm dev:fastify # Run examples/fastify dev server
|
|
9
|
+
pnpm dev:hono # Run examples/hono dev server
|
|
10
|
+
pnpm dev:koa # Run examples/koa dev server
|
|
11
|
+
pnpm dev:ssr # Run examples/ssr dev server
|
|
12
|
+
pnpm lint # Lint + typecheck (deno lint + tsc)
|
|
13
|
+
pnpm test # Run tests with coverage
|
|
14
|
+
pnpm test-ui # Run tests with UI
|
|
15
|
+
pnpm lint:ts # deno lint src
|
|
16
|
+
pnpm fix:ts # deno lint src --fix
|
|
17
|
+
pnpm check:ts # tsc -noEmit
|
|
18
|
+
pnpm format # deno fmt src
|
|
19
|
+
pnpm build # tsdown (outputs to dist/)
|
|
20
|
+
pnpm up:examples # Update all example deps
|
|
21
|
+
pnpm up:src # Update all src deps
|
|
22
|
+
pnpm up:root # Update root deps
|
|
23
|
+
pnpm upd # Update all deps (up:examples + up:src + up:root)
|
|
24
|
+
pnpm prepublishOnly # upd + lint + check:ts + format + build
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Build Order
|
|
28
|
+
|
|
29
|
+
`lint -> check:ts -> format -> build` (verified in `prepublishOnly`)
|
|
30
|
+
|
|
31
|
+
## Examples
|
|
32
|
+
|
|
33
|
+
The `examples/` directory contains 6 example apps:
|
|
34
|
+
|
|
35
|
+
| Example | Adapter | Type | Run Command | Config |
|
|
36
|
+
| -----------| -------------------------------------------------------------------| ------| --------------------| ----------------------------------|
|
|
37
|
+
| `spa` | Vite dev server (no adapter) | SPA | `pnpm dev` | `examples/spa/rpc.config.ts` |
|
|
38
|
+
| `express` | Express | SSR | `pnpm dev:express` | `examples/express/rpc.config.ts` |
|
|
39
|
+
| `fastify` | Fastify | SSR | `pnpm dev:fastify` | `examples/fastify/rpc.config.ts` |
|
|
40
|
+
| `hono` | Hono | SSR | `pnpm dev:hono` | `examples/hono/rpc.config.ts` |
|
|
41
|
+
| `koa` | Koa | SSR | `pnpm dev:koa` | `examples/koa/rpc.config.ts` |
|
|
42
|
+
| `ssr` | Custom `http-express.ts` (Express-compatible `node:http` server ) | SSR | `pnpm dev:ssr` | `examples/ssr/rpc.config.ts` |
|
|
43
|
+
|
|
44
|
+
Each example follows the same structure:
|
|
45
|
+
|
|
46
|
+
- `server.js` / `server.ts` — production server with Vite middleware in dev, static serving in prod
|
|
47
|
+
- `src/api/server.ts` — RPC server functions (auto-scanned by the plugin)
|
|
48
|
+
- `src/entry-server.ts` — SSR entry (hydrates on client)
|
|
49
|
+
- `src/entry-client.ts` — Client entry
|
|
50
|
+
- `vite.config.ts` + `rpc.config.ts` — Configuration files
|
|
51
|
+
|
|
52
|
+
## Key Directories
|
|
53
|
+
|
|
54
|
+
- `src/` — source for all packages (vite plugin, server, express, fastify, hono, koa adapters)
|
|
55
|
+
- `dist/` — build output (not committed, generated by tsdown)
|
|
56
|
+
- `tests/` — test files (one per adapter + plugin)
|
|
57
|
+
- `tests/fixtures/` — test fixtures (config files, vite-mock.ts)
|
|
58
|
+
- `examples/` — example apps (spa, express, fastify, hono, koa, ssr)
|
|
59
|
+
|
|
60
|
+
## Build Output (tsdown)
|
|
61
|
+
|
|
62
|
+
The tsdown.config.ts produces multiple entries:
|
|
63
|
+
|
|
64
|
+
- `dist/index.mjs` — main Vite plugin
|
|
65
|
+
- `dist/server/server.mjs` — standalone server
|
|
66
|
+
- `dist/express/express.mjs` — Express middleware
|
|
67
|
+
- `dist/fastify/fastify.mjs` — Fastify middleware
|
|
68
|
+
- `dist/hono/hono.mjs` — Hono middleware
|
|
69
|
+
- `dist/koa/koa.mjs` — Koa middleware
|
|
70
|
+
|
|
71
|
+
## Test Files
|
|
72
|
+
|
|
73
|
+
| File | Tests | |
|
|
74
|
+
| -------------------------| --------------------------------------------------------------------| -----|
|
|
75
|
+
| `tests/plugin.test.ts` | Plugin init, loadRPCConfig, createServerFunction, getClientModules | |
|
|
76
|
+
| `tests/express.test.ts` | Express helpers, createMiddleware, createRPCMiddleware | |
|
|
77
|
+
| `tests/fastify.test.ts` | Fastify helpers, plugin, createMiddleware, createRPCMiddleware | |
|
|
78
|
+
| `tests/hono.test.ts` | Hono helpers, createMiddleware, createRPCMiddleware | |
|
|
79
|
+
| `tests/koa.test.ts` | Koa helpers, createMiddleware, createRPCMiddleware | |
|
|
80
|
+
| `tests/scan.test.ts` | scanForServerFiles (real scan, skip, devServer, error handling) | |
|
|
81
|
+
|
|
82
|
+
## Important Notes
|
|
83
|
+
|
|
84
|
+
- In dev mode, **only** the Vite dev server and Express/Connect middleware are available
|
|
85
|
+
- Uses `deno` for linting and formatting (not eslint/prettier)
|
|
86
|
+
- Uses `tsdown` for bundling (not rollup/vite directly)
|
|
87
|
+
- Uses `vitest` for testing with `istanbul` coverage
|
|
88
|
+
- The `iscScanned` module-level flag in `scanForServerFiles.ts` persists across tests — use `devServer` parameter or reset modules to bypass
|
|
89
|
+
|
|
90
|
+
## Framework
|
|
91
|
+
|
|
92
|
+
- Vite plugin for creating server functions with automatic RPC generation
|
|
93
|
+
- Server functions return `{ data: Promise<T>, cancel: (reason?: string) => void }` shape
|
|
94
|
+
- Framework-agnostic core with adapters for Express, Fastify, Hono, and Koa
|
|
95
|
+
- Client modules are auto-generated with `AbortController` support for cancellation
|
|
96
|
+
- Server-side caching must be handled by third party tools (e.g. `@tanstack/react-query`)
|
|
97
|
+
|
|
98
|
+
## Security & Hardening
|
|
99
|
+
|
|
100
|
+
- **Prefix boundary check**: All adapters use `new RegExp(\`^/${escapeRegExp(rpcPreffix)}/\`)` instead of `startsWith` to prevent path segment bypassing (e.g., `/__rpc-evil/foo` no longer matches prefix `"__rpc"`)
|
|
101
|
+
- **Prefix regex injection prevention**: `rpcPreffix` config string is escaped via `escapeRegExp()` before being embedded in the boundary regex, preventing ReDoS or unintended matching from metacharacters in the prefix
|
|
102
|
+
- **Regex compilation hoisted**: All prefix/path regexes are compiled once at middleware creation time (not per-request), eliminating per-request regex overhead
|
|
103
|
+
- **Koa URL normalization**: Koa adapter parses `ctx.url` through `new URL()` to strip query strings and normalize encoding before prefix checking
|
|
104
|
+
- **Code injection prevention in client module generation**: `getClientModules.ts` validates all interpolated identifiers (`fnName`, `fnEntry`, `rpcPreffix`) against `/^[A-Za-z_$][A-Za-z0-9_$]*$/` (and a path-safe variant allowing `/`) before interpolating into the generated client bundle. This prevents code injection via malicious export names or prefixes containing template literal interpolations (`${...}`), backticks, or `</script>` sequences.
|
|
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
|
+
- **Generic 404 responses**: Error messages no longer echo the requested function name, preventing function enumeration
|
|
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.
|
|
108
|
+
- **No client-side secrets or stack traces**: Error responses always return `"Internal Server Error"` regardless of the underlying error; `console.error(String(err))` is server-side only for debugging and does not surface internals to the client
|
|
109
|
+
|
|
110
|
+
## Threat Model
|
|
111
|
+
|
|
112
|
+
The framework's security boundary is the **RPC prefix-gated HTTP endpoint**. Inputs:
|
|
113
|
+
|
|
114
|
+
| Input | Source | Trust Level | Hardening Applied |
|
|
115
|
+
| -----------------------| -------------------------------| ---------------------| --------------------------------------------------------------------------|
|
|
116
|
+
| `rpcPreffix` (config) | `rpc.config.ts` / dev options | Developer-trusted | Escaped before regex; validated before code gen |
|
|
117
|
+
| Function export name | `src/api/server.ts` exports | Developer-trusted | Validated against identifier regex before client codegen |
|
|
118
|
+
| HTTP request URL | Untrusted client | Boundary-filtered | Prefix regex (escaped, anchored, hoisted); Koa URL normalization |
|
|
119
|
+
| HTTP request body | Untrusted client | Capped by framework | Framework body parsers cap JSON and raw bodies |
|
|
120
|
+
|
|
121
|
+
**Attackers cannot**:
|
|
122
|
+
- Inject code via the prefix or function names (validated identifiers)
|
|
123
|
+
- Bypass the prefix via segment-prefixing tricks (anchored regex, not `startsWith`)
|
|
124
|
+
- Trigger ReDoS via prefix metacharacters (escaped before compilation)
|
|
125
|
+
- Exhaust memory via large raw text bodies (framework body parsers enforce limits)
|
|
126
|
+
|
|
127
|
+
**Attackers are expected to**:
|
|
128
|
+
- Be free to send as many requests as the host allows (no rate limiting — host's responsibility)
|
|
129
|
+
- Be free to hit any URL (no auth — host's responsibility via prior middleware)
|
|
130
|
+
- Be rejected with generic errors (no enumeration, no stack traces)
|
|
131
|
+
|
|
132
|
+
## Documentation
|
|
133
|
+
|
|
134
|
+
- `wiki/index.md` — Overview
|
|
135
|
+
- `wiki/getting-started.md` — Quick start
|
|
136
|
+
- `wiki/setup.md` — Project structure and configuration
|
|
137
|
+
- `wiki/server-functions.md` — Creating server functions
|
|
138
|
+
- `wiki/client-usage.md` — Client-side usage
|
|
139
|
+
- `wiki/configuration.md` — Configuration reference
|
|
140
|
+
- `wiki/adapters.md` — Framework adapters
|
|
141
|
+
- `wiki/security.md` — Security hardening
|
|
142
|
+
- `wiki/best-practices.md` — Tips and best practices
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 thednp
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# @thednp/rpc
|
|
2
|
+
|
|
3
|
+
[](https://coveralls.io/github/thednp/rpcv)
|
|
4
|
+
[](https://github.com/thednp/rpcv/actions/workflows/ci.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/@thednp/rpc)
|
|
6
|
+
[](http://npm-stat.com/charts.html?package=@thednp/rpc)
|
|
7
|
+
|
|
8
|
+
A Vite plugin for automatic RPC generation that is simple, framework agnostic and very simple to use.
|
|
9
|
+
|
|
10
|
+
* Server functions defined in `src/api/server.ts` are auto-scanned, with client modules generated at build time.
|
|
11
|
+
* Typed client `fetch` based modules are generated and available via the RPC plugin.
|
|
12
|
+
|
|
13
|
+
The name stands for RPC via Vite, because that's exactly what it is. No more, no less.
|
|
14
|
+
|
|
15
|
+
## Why this exists
|
|
16
|
+
|
|
17
|
+
Most RPC solutions ask you to adopt a new way of thinking. You learn a complex API, you organize your code into a specific structure, for sure they are powerful, they work well and provide excelent DX, but complexity comes with its own drawbacks.
|
|
18
|
+
|
|
19
|
+
`@thednp/rpc` takes the opposite bet: your **server functions should just be functions**. You define them in a file, import and call them where you need them. The plugin handles everything in between — system wide configuration, scanning, type inference, client stub generation, middleware registration, request cancellation — without asking you to restructure your codebase or learn a new DSL (Domain-Specific Language).
|
|
20
|
+
|
|
21
|
+
### The Mental model
|
|
22
|
+
* **Query Engine** The Brain (something like `@tanstack/react-query` that handles caching, lifecycles, deduplication).
|
|
23
|
+
* **@thednp/rpc** The Nervous System (transport, serialization, client/server bridge, request cancellation).
|
|
24
|
+
* **UI Framework** = The Muscle (Reactive DOM updates).
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- Framework-agnostic core with adapters for **Express**, **Fastify**, **Hono**, and **Koa**
|
|
29
|
+
- Automatic RPC generation — server functions are auto-scanned; client `fetch` based modules are generated at build time
|
|
30
|
+
- File-level server code isolation (no `'use server'` directives required)
|
|
31
|
+
- System-wide configuration via `rpc.config.ts`
|
|
32
|
+
- `AbortController` based request cancellation (via `.cancel()` on the returned handle)
|
|
33
|
+
- TypeScript support with generic type inference
|
|
34
|
+
|
|
35
|
+
## Demos
|
|
36
|
+
|
|
37
|
+
| Example | Source Code | Try online |
|
|
38
|
+
| -----------------| ---------------------------------------------------------------------------------| -------------------------------------------------------------------------------------------|
|
|
39
|
+
| SPA - node:http | [examples/spa](https://github.com/thednp/rpcv/tree/master/examples/spa) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpcv/tree/master/examples/spa) |
|
|
40
|
+
| SSR - node:http | [examples/ssr](https://github.com/thednp/rpcv/tree/master/examples/ssr) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpcv/tree/master/examples/ssr) |
|
|
41
|
+
| Express | [examples/express](https://github.com/thednp/rpcv/tree/master/examples/express) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpcv/tree/master/examples/express) |
|
|
42
|
+
| Fastify | [examples/fastify](https://github.com/thednp/rpcv/tree/master/examples/fastify) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpcv/tree/master/examples/fastify) |
|
|
43
|
+
| Hono | [examples/hono](https://github.com/thednp/rpcv/tree/master/examples/hono) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpcv/tree/master/examples/hono) |
|
|
44
|
+
| Koa | [examples/koa](https://github.com/thednp/rpcv/tree/master/examples/koa) | [StackBlitz](https://stackblitz.com/fork/github/thednp/rpcv/tree/master/examples/koa) |
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
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` |
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
### 1. Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pnpm add @thednp/rpc@latest
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2. Configuration
|
|
67
|
+
|
|
68
|
+
Create `rpc.config.ts` at your project root:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { defineConfig } from "@thednp/rpc";
|
|
72
|
+
|
|
73
|
+
export default defineConfig({
|
|
74
|
+
adapter: "express",
|
|
75
|
+
rpcPreffix: "__rpc",
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Update `vite.config.ts` at your project root:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { defineConfig } from 'vite';
|
|
83
|
+
import rpc from '@thednp/rpc';
|
|
84
|
+
|
|
85
|
+
export default defineConfig({
|
|
86
|
+
plugins: [rpc(/* development options */)]
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 3. Define a server function
|
|
92
|
+
|
|
93
|
+
Create `src/api/server.ts`:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { createServerFunction } from "@thednp/rpc/server";
|
|
97
|
+
|
|
98
|
+
export const greet = createServerFunction("greet", (signal, name: string) => {
|
|
99
|
+
// access AbortSignal
|
|
100
|
+
signal.throwIfAborted();
|
|
101
|
+
|
|
102
|
+
// add validation and other server ONLY functionality
|
|
103
|
+
|
|
104
|
+
// return the result of processing
|
|
105
|
+
return `Hello, ${name}!`;
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Create `src/api/index.ts`:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
export * from "./server";
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 4. Call it from the client
|
|
116
|
+
|
|
117
|
+
Import the generated client module in any client-side file:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
// src/app.ts
|
|
121
|
+
import { greet } from "./api";
|
|
122
|
+
|
|
123
|
+
const { data, cancel } = greet("World");
|
|
124
|
+
const result = await data; // "Hello, World!"
|
|
125
|
+
cancel(); // AbortController-based cancellation
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 5. Register the RPC middleware on the server
|
|
129
|
+
|
|
130
|
+
Import and use the middleware from your chosen adapter package. See the [Adapters guide](./wiki/adapters.md) for full snippets for each framework.
|
|
131
|
+
|
|
132
|
+
## Testing
|
|
133
|
+
|
|
134
|
+
### Unit Testing
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
pnpm test # Run tests with coverage
|
|
138
|
+
pnpm test-ui # Run tests with UI
|
|
139
|
+
pnpm test --run # Single run
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Tests use **Vitest** with **Istanbul** coverage. There are 6 test files covering all adapters plus the plugin.
|
|
143
|
+
|
|
144
|
+
### Live Testing
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
pnpm test-dev # Runs all examples/<example> in DEV mode and reports their status in a table
|
|
148
|
+
pnpm test-prod # Runs all examples/<example> in PRODUCTION mode and reports their status in a table
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
These tests check the following:
|
|
152
|
+
* check if the server runs and doesn't crash
|
|
153
|
+
* check if there is any issue generating the HTML
|
|
154
|
+
* check if server functions work properly
|
|
155
|
+
|
|
156
|
+
## Security
|
|
157
|
+
|
|
158
|
+
- Prefix boundary check via anchored regex — prevents `/__rpc-evil/foo` bypass
|
|
159
|
+
- Body size limit: `readBody` caps raw text/plain streams at 1 MiB by default
|
|
160
|
+
- Code injection prevention: all interpolated identifiers are validated before client module generation
|
|
161
|
+
- Generic error responses — no stack traces or internal details exposed to the client
|
|
162
|
+
|
|
163
|
+
See [Security](./wiki/security.md) for full details.
|
|
164
|
+
|
|
165
|
+
## Documentation
|
|
166
|
+
|
|
167
|
+
- [Getting Started](./wiki/getting-started.md)
|
|
168
|
+
- [Setup Guide](./wiki/setup.md)
|
|
169
|
+
- [Configuration](./wiki/configuration.md)
|
|
170
|
+
- [Server Functions](./wiki/server-functions.md)
|
|
171
|
+
- [Client Usage](./wiki/client-usage.md)
|
|
172
|
+
- [Adapters](./wiki/adapters.md)
|
|
173
|
+
- [Best Practices](./wiki/best-practices.md)
|
|
174
|
+
- [Security](./wiki/security.md)
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
Released under [MIT](LICENSE).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Connect, ViteDevServer } from "vite";
|
|
2
|
+
import { BodyResult, JsonValue, MiddlewareOptions, RpcPluginOptions } from "@thednp/rpc";
|
|
3
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
4
|
+
import { Express, NextFunction, Request, Response } from "express";
|
|
5
|
+
//#region src/express/types.d.ts
|
|
6
|
+
type ExpressMiddlewareOptions = MiddlewareOptions<"express">;
|
|
7
|
+
type ExpressMiddlewareFn = <A extends RpcPluginOptions["adapter"] = "express">(initialOptions?: Partial<ExpressMiddlewareOptions>) => ExpressMiddlewareHooks["handler"];
|
|
8
|
+
interface ExpressMiddlewareHooks {
|
|
9
|
+
handler: (req: IncomingMessage | Request, res: ServerResponse | Response, next: Connect.NextFunction | NextFunction) => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/express/createMiddleware.d.ts
|
|
13
|
+
declare const createMiddleware: ExpressMiddlewareFn;
|
|
14
|
+
declare const createRPCMiddleware: ExpressMiddlewareFn;
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/express/helpers.d.ts
|
|
17
|
+
declare function attachRPC(app: Express): Promise<void>;
|
|
18
|
+
declare function attachVite(app: Express, vite: ViteDevServer): void;
|
|
19
|
+
declare const readBody: (req: Request | IncomingMessage) => Promise<BodyResult>;
|
|
20
|
+
declare const isExpressRequest: (req: IncomingMessage | Request) => req is Request;
|
|
21
|
+
declare const isExpressResponse: (res: ServerResponse | Response) => res is Response;
|
|
22
|
+
declare const hasPreParsedBody: (req: IncomingMessage | Request) => req is Request;
|
|
23
|
+
declare const getRequestDetails: (request: Request | IncomingMessage) => {
|
|
24
|
+
url: string;
|
|
25
|
+
search: string;
|
|
26
|
+
searchParams: URLSearchParams;
|
|
27
|
+
headers: import("node:http").IncomingHttpHeaders;
|
|
28
|
+
method: string | undefined;
|
|
29
|
+
};
|
|
30
|
+
declare const getResponseDetails: (response: Response | ServerResponse) => {
|
|
31
|
+
isResponseSent: boolean;
|
|
32
|
+
setHeader: (name: string, value: string) => void;
|
|
33
|
+
statusCode: number;
|
|
34
|
+
setStatusCode: (code: number) => void;
|
|
35
|
+
sendResponse: (code: number, output: Record<string, JsonValue>) => void;
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
export { type ExpressMiddlewareFn, type ExpressMiddlewareHooks, type ExpressMiddlewareOptions, attachRPC, attachVite, createMiddleware, createRPCMiddleware, getRequestDetails, getResponseDetails, hasPreParsedBody, isExpressRequest, isExpressResponse, readBody };
|
|
39
|
+
//# sourceMappingURL=express.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.d.mts","names":[],"sources":["../../src/express/types.d.ts","../../src/express/createMiddleware.ts","../../src/express/helpers.ts"],"mappings":";;;;;KAKY,2BAA2B;KAE3B,uBACV,UAAU,yCAEV,iBAAiB,QAAQ,8BACtB;UAEY;EACf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiB,UACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;cCGM,kBAAkB;cAmElB,qBAAqB;;;iBC5EZ,UAAU,KAAK,UAAO;iBAM5B,WAAW,KAAK,SAAS,MAAM;cAKlC,WAAQ,KACd,UAAiB,oBACrB,QAAQ;cAsDE,mBAAgB,KACtB,kBAAkB,YACtB,OAAO;cAIG,oBAAiB,KACvB,iBAAiB,aACrB,OAAO;cAIG,mBAAgB,KACtB,kBAAkB,YACtB,OAAO;cAIG,oBAAiB,SACnB,UAAiB;EAQxB;EACA;EACA,cAAY;EACZ,6BAAO;EACP;;cAIS,qBAAkB,UACnB,WAAkB;;EAIH,YAAA,cAAM;;EAQF,gBAAA;EAQD,eAAA,cAAM,QAAU,eAAe"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
|
|
2
|
+
//#region src/options.ts
|
|
3
|
+
const defaultRPCOptions = {
|
|
4
|
+
rpcPreffix: "__rpc",
|
|
5
|
+
adapter: "express"
|
|
6
|
+
};
|
|
7
|
+
const defaultMiddlewareOptions = {
|
|
8
|
+
rpcPreffix: void 0,
|
|
9
|
+
path: void 0
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/express/helpers.ts
|
|
13
|
+
async function attachRPC(app) {
|
|
14
|
+
const { loadRPCConfig } = await import("@thednp/rpc");
|
|
15
|
+
const { adapter: _adapter, ...options } = await loadRPCConfig();
|
|
16
|
+
app.use(createRPCMiddleware(options));
|
|
17
|
+
}
|
|
18
|
+
function attachVite(app, vite) {
|
|
19
|
+
app.use(vite.middlewares);
|
|
20
|
+
}
|
|
21
|
+
const readBody = (req) => {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
if (hasPreParsedBody(req) && req.body !== void 0) {
|
|
24
|
+
const isJSON = (req.headers["content-type"]?.toLowerCase() || "").includes("json");
|
|
25
|
+
resolve({
|
|
26
|
+
contentType: isJSON ? "application/json" : "text/plain",
|
|
27
|
+
data: isJSON ? req.body : String(req.body)
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
let body = "";
|
|
32
|
+
const toggleListeners = (add) => {
|
|
33
|
+
const method = add ? "on" : "off";
|
|
34
|
+
req[method]("data", onData);
|
|
35
|
+
req[method]("end", onEnd);
|
|
36
|
+
req[method]("error", onError);
|
|
37
|
+
};
|
|
38
|
+
const onData = (chunk) => {
|
|
39
|
+
body += chunk.toString();
|
|
40
|
+
};
|
|
41
|
+
const onEnd = () => {
|
|
42
|
+
toggleListeners();
|
|
43
|
+
const isJSON = (req.headers["content-type"]?.toLowerCase() || "").includes("json");
|
|
44
|
+
try {
|
|
45
|
+
const data = JSON.parse(body);
|
|
46
|
+
resolve({
|
|
47
|
+
contentType: isJSON ? "application/json" : "text/plain",
|
|
48
|
+
data
|
|
49
|
+
});
|
|
50
|
+
} catch (_e) {
|
|
51
|
+
resolve({
|
|
52
|
+
contentType: "text/plain",
|
|
53
|
+
data: String(body)
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const onError = (err) => {
|
|
58
|
+
toggleListeners();
|
|
59
|
+
reject(err);
|
|
60
|
+
};
|
|
61
|
+
toggleListeners(true);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
const isExpressRequest = (req) => {
|
|
65
|
+
return "originalUrl" in req;
|
|
66
|
+
};
|
|
67
|
+
const isExpressResponse = (res) => {
|
|
68
|
+
return "json" in res && "send" in res;
|
|
69
|
+
};
|
|
70
|
+
const hasPreParsedBody = (req) => {
|
|
71
|
+
return "body" in req;
|
|
72
|
+
};
|
|
73
|
+
const getRequestDetails = (request) => {
|
|
74
|
+
const rawUrl = isExpressRequest(request) ? request.originalUrl : request.url;
|
|
75
|
+
const url = new URL(rawUrl, "http://localhost");
|
|
76
|
+
return {
|
|
77
|
+
url: url.pathname,
|
|
78
|
+
search: url.search,
|
|
79
|
+
searchParams: url.searchParams,
|
|
80
|
+
headers: request.headers,
|
|
81
|
+
method: request.method
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
const getResponseDetails = (response) => {
|
|
85
|
+
const isResponseSent = response.headersSent || response.writableEnded;
|
|
86
|
+
const setHeader = (name, value) => {
|
|
87
|
+
if (isExpressResponse(response)) response.header(name, value);
|
|
88
|
+
else response.setHeader(name, value);
|
|
89
|
+
};
|
|
90
|
+
const setStatusCode = (code) => {
|
|
91
|
+
if (isExpressResponse(response)) response.status(code);
|
|
92
|
+
else response.statusCode = code;
|
|
93
|
+
};
|
|
94
|
+
const sendResponse = (code, output) => {
|
|
95
|
+
setStatusCode(code);
|
|
96
|
+
setHeader("Content-Type", "application/json");
|
|
97
|
+
if (isExpressResponse(response)) response.send(JSON.stringify(output));
|
|
98
|
+
else response.end(JSON.stringify(output));
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
isResponseSent,
|
|
102
|
+
setHeader,
|
|
103
|
+
statusCode: response.statusCode,
|
|
104
|
+
setStatusCode,
|
|
105
|
+
sendResponse
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/tools.ts
|
|
110
|
+
function escapeRegExp(s) {
|
|
111
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/express/createMiddleware.ts
|
|
115
|
+
let middlewareCount = 0;
|
|
116
|
+
const middlewareStack = /* @__PURE__ */ new Set();
|
|
117
|
+
const createMiddleware = (initialOptions = {}) => {
|
|
118
|
+
const options = Object.assign({}, defaultMiddlewareOptions, initialOptions);
|
|
119
|
+
const middlewareName = options.name;
|
|
120
|
+
const rpcPreffix = options.rpcPreffix;
|
|
121
|
+
const path = options.path;
|
|
122
|
+
const handler = options.handler;
|
|
123
|
+
let name = middlewareName;
|
|
124
|
+
if (!name) {
|
|
125
|
+
name = "viteRPCMiddleware-" + middlewareCount;
|
|
126
|
+
middlewareCount += 1;
|
|
127
|
+
}
|
|
128
|
+
if (middlewareStack.has(name)) throw new Error(`The middleware name "${name}" is already used.`);
|
|
129
|
+
middlewareStack.add(name);
|
|
130
|
+
const prefixRegex = rpcPreffix ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`) : null;
|
|
131
|
+
const pathMatcher = path ? typeof path === "string" ? new RegExp(path) : path : null;
|
|
132
|
+
const middlewareHandler = async (req, _res, next) => {
|
|
133
|
+
const { url } = getRequestDetails(req);
|
|
134
|
+
if (serverFunctionsMap.size === 0) await scanForServerFiles();
|
|
135
|
+
if (!handler) return next?.();
|
|
136
|
+
if (pathMatcher && !pathMatcher.test(url)) return next?.();
|
|
137
|
+
if (prefixRegex && !prefixRegex.test(url)) return next?.();
|
|
138
|
+
await handler(req, _res, next);
|
|
139
|
+
};
|
|
140
|
+
Object.defineProperty(middlewareHandler, "name", { value: name });
|
|
141
|
+
return middlewareHandler;
|
|
142
|
+
};
|
|
143
|
+
const createRPCMiddleware = (initialOptions = {}) => {
|
|
144
|
+
const options = Object.assign({}, defaultMiddlewareOptions, { rpcPreffix: defaultRPCOptions.rpcPreffix }, initialOptions);
|
|
145
|
+
const rpcPreffix = options.rpcPreffix;
|
|
146
|
+
const prefixRegex = rpcPreffix ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`) : null;
|
|
147
|
+
const prefixReplace = `/${rpcPreffix}/`;
|
|
148
|
+
return createMiddleware({
|
|
149
|
+
...options,
|
|
150
|
+
handler: async (req, res, _next) => {
|
|
151
|
+
const { url } = getRequestDetails(req);
|
|
152
|
+
const { sendResponse } = getResponseDetails(res);
|
|
153
|
+
if (prefixRegex && !prefixRegex.test(url)) return;
|
|
154
|
+
const functionName = url.replace(prefixReplace, "");
|
|
155
|
+
const serverFunction = serverFunctionsMap.get(functionName);
|
|
156
|
+
if (!serverFunction) {
|
|
157
|
+
sendResponse(404, { error: "Function not found" });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const body = await readBody(req);
|
|
162
|
+
const args = Array.isArray(body.data) ? body.data : [body.data];
|
|
163
|
+
const { data, cancel } = serverFunction.handler(...args);
|
|
164
|
+
const onClose = () => cancel("client disconnected");
|
|
165
|
+
req.on("close", onClose);
|
|
166
|
+
const result = await data;
|
|
167
|
+
req.off("close", onClose);
|
|
168
|
+
if (!res.headersSent) sendResponse(200, { data: result });
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error(String(err));
|
|
171
|
+
sendResponse(500, { error: "Internal Server Error" });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
//#endregion
|
|
177
|
+
export { attachRPC, attachVite, createMiddleware, createRPCMiddleware, getRequestDetails, getResponseDetails, hasPreParsedBody, isExpressRequest, isExpressResponse, readBody };
|
|
178
|
+
|
|
179
|
+
//# sourceMappingURL=express.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.mjs","names":[],"sources":["../../src/options.ts","../../src/express/helpers.ts","../../src/tools.ts","../../src/express/createMiddleware.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions = {\n contentType: \"application/json\",\n} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPreffix: \"__rpc\",\n adapter: \"express\",\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPreffix: undefined,\n path: undefined,\n} satisfies MiddlewareOptions;\n","// src/express/helpers.ts\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type {\n Request as ExpressRequest,\n Response as ExpressResponse,\n} from \"express\";\nimport type { BodyResult, JsonValue } from \"@thednp/rpc\";\nimport { Buffer } from \"node:buffer\";\nimport type { ViteDevServer } from \"vite\";\nimport type { Express } from \"express\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\nexport async function attachRPC(app: Express) {\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n app.use(createRPCMiddleware(options));\n}\n\nexport function attachVite(app: Express, vite: ViteDevServer) {\n app.use(vite.middlewares);\n}\n\n// src/express/helpers.ts\nexport const readBody = (\n req: ExpressRequest | IncomingMessage,\n): Promise<BodyResult> => {\n return new Promise((resolve, reject) => {\n // If an Express body parser already consumed the stream\n // via app.use(express.json()), use req.body directly\n if (hasPreParsedBody(req) && req.body !== undefined) {\n // istanbul ignore next\n const contentType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const isJSON = contentType.includes(\"json\");\n resolve({\n contentType: isJSON ? \"application/json\" : \"text/plain\",\n data: isJSON ? req.body : String(req.body),\n } as BodyResult);\n return;\n }\n\n // Else we parse the body right away\n let body = \"\";\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n req[method](\"data\", onData);\n req[method](\"end\", onEnd);\n req[method](\"error\", onError);\n };\n\n const onData = (chunk: Buffer) => {\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const incomingType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const isJSON = incomingType.includes(\"json\");\n try {\n const data = JSON.parse(body);\n resolve({\n contentType: isJSON ? \"application/json\" : \"text/plain\",\n data,\n });\n } catch (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n\n const onError = (err: Error) => {\n toggleListeners();\n\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n\nexport const isExpressRequest = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"originalUrl\" in req;\n};\n\nexport const isExpressResponse = (\n res: ServerResponse | ExpressResponse,\n): res is ExpressResponse => {\n return \"json\" in res && \"send\" in res;\n};\n\nexport const hasPreParsedBody = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"body\" in req;\n};\n\nexport const getRequestDetails = (\n request: ExpressRequest | IncomingMessage,\n) => {\n const rawUrl = (\n isExpressRequest(request) ? request.originalUrl : request.url\n ) as string;\n const url = new URL(rawUrl, \"http://localhost\");\n\n return {\n url: url.pathname,\n search: url.search,\n searchParams: url.searchParams,\n headers: request.headers,\n method: request.method,\n };\n};\n\nexport const getResponseDetails = (\n response: ExpressResponse | ServerResponse,\n) => {\n const isResponseSent = response.headersSent || response.writableEnded;\n\n const setHeader = (name: string, value: string) => {\n if (isExpressResponse(response)) {\n response.header(name, value);\n } else {\n response.setHeader(name, value);\n }\n };\n\n const setStatusCode = (code: number) => {\n if (isExpressResponse(response)) {\n response.status(code);\n } else {\n response.statusCode = code;\n }\n };\n\n const sendResponse = (code: number, output: Record<string, JsonValue>) => {\n setStatusCode(code);\n setHeader(\"Content-Type\", \"application/json\");\n\n if (isExpressResponse(response)) {\n response.send(JSON.stringify(output));\n } else {\n response.end(JSON.stringify(output));\n }\n };\n\n return {\n isResponseSent,\n setHeader,\n statusCode: response.statusCode,\n setStatusCode,\n sendResponse,\n };\n};\n","export function escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n","// src/express/createMidleware.ts\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type {\n NextFunction,\n Request as ExpressRequest,\n Response as ExpressResponse,\n} from \"express\";\nimport type {\n ExpressMiddlewareFn,\n ExpressMiddlewareOptions,\n} from \"./types.d.ts\";\nimport type { Connect } from \"vite\";\n// import type { JsonArray, JsonValue } from \"@thednp/rpc\";\nimport { scanForServerFiles, serverFunctionsMap } from \"@thednp/rpc/server\";\nimport { defaultMiddlewareOptions, defaultRPCOptions } from \"../options.ts\";\nimport { getRequestDetails, getResponseDetails, readBody } from \"./helpers.ts\";\nimport { escapeRegExp } from \"../tools.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\nexport const createMiddleware: ExpressMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as ExpressMiddlewareOptions;\n const middlewareName = options.name;\n const rpcPreffix = options.rpcPreffix;\n const path = options.path;\n const handler = options.handler;\n\n let name = middlewareName;\n if (!name) {\n name = \"viteRPCMiddleware-\" + middlewareCount;\n middlewareCount += 1;\n }\n if (middlewareStack.has(name)) {\n throw new Error(`The middleware name \"${name}\" is already used.`);\n }\n middlewareStack.add(name);\n\n // Hoist regex compilation out of per-request path. Escape the prefix to\n // prevent regex injection via metacharacters in the config string.\n const prefixRegex: RegExp | null = rpcPreffix\n ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`)\n : null;\n const pathMatcher: RegExp | null = path\n ? (typeof path === \"string\" ? new RegExp(path) : path)\n : null;\n\n const middlewareHandler = async (\n req: IncomingMessage | ExpressRequest,\n _res: ServerResponse | ExpressResponse,\n next: Connect.NextFunction | NextFunction,\n ) => {\n const { url } = getRequestDetails(req);\n\n // When serving from production server, scan for server files\n if (serverFunctionsMap.size === 0) {\n await scanForServerFiles();\n }\n\n // No need to continue when no handler provided\n if (!handler) {\n return next?.();\n }\n\n // Path matching\n if (pathMatcher && !pathMatcher.test(url)) return next?.();\n\n // rpcPreffix matching (boundary-safe via escaped regex)\n if (prefixRegex && !prefixRegex.test(url)) {\n return next?.();\n }\n\n // Execute handler\n await handler(req, _res, next);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n// Create RPC middleware\nexport const createRPCMiddleware: ExpressMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPreffix: defaultRPCOptions.rpcPreffix },\n initialOptions,\n ) as ExpressMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const rpcPreffix = options.rpcPreffix;\n const prefixRegex = rpcPreffix\n ? new RegExp(`^/${escapeRegExp(rpcPreffix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${rpcPreffix}/`;\n\n return createMiddleware({\n ...options,\n handler: async (\n req: IncomingMessage | ExpressRequest,\n res: ServerResponse | ExpressResponse,\n _next: NextFunction | Connect.NextFunction,\n ) => {\n const { url } = getRequestDetails(req);\n const { sendResponse } = getResponseDetails(res);\n\n // Validate the url starts with the prefix via the escaped regex\n // istanbul ignore next\n if (prefixRegex && !prefixRegex.test(url)) {\n // falls through to next handler (never reached in practice; the outer\n // createMiddleware already gates on this, but kept for defense-in-depth)\n return;\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = serverFunctionsMap.get(functionName);\n\n if (!serverFunction) {\n sendResponse(404, { error: \"Function not found\" });\n return;\n }\n\n try {\n const body = await readBody(req);\n const args = Array.isArray(body.data) ? body.data : [body.data];\n const { data, cancel } = serverFunction.handler(...args);\n const onClose = () => cancel(\"client disconnected\");\n\n req.on(\"close\", onClose);\n const result = await data;\n req.off(\"close\", onClose);\n\n // istanbul ignore else\n if (!res.headersSent) sendResponse(200, { data: result });\n } catch (err) {\n console.error(String(err));\n sendResponse(500, { error: \"Internal Server Error\" });\n }\n },\n });\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { FastifyInstance, FastifyReply, FastifyRequest, HookHandlerDoneFunction } from "fastify";
|
|
2
|
+
import { MiddlewareOptions, RpcPluginOptions } from "@thednp/rpc";
|
|
3
|
+
import { ViteDevServer } from "vite";
|
|
4
|
+
import "express";
|
|
5
|
+
import "hono";
|
|
6
|
+
import "@hono/node-server";
|
|
7
|
+
import "koa";
|
|
8
|
+
//#region src/fastify/types.d.ts
|
|
9
|
+
type FastifyMiddlewareOptions = MiddlewareOptions<"fastify">;
|
|
10
|
+
type FastifyMiddlewareFn = <A extends RpcPluginOptions["adapter"] = "fastify">(initialOptions?: Partial<FastifyMiddlewareOptions>) => FastifyMiddlewareHooks["handler"];
|
|
11
|
+
interface FastifyMiddlewareHooks {
|
|
12
|
+
handler: (req: FastifyRequest, res: FastifyReply, done: HookHandlerDoneFunction) => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
// Define the plugin function
|
|
15
|
+
type RpcFastifyPluginOptions = MiddlewareOptions<"fastify"> & {
|
|
16
|
+
isRPC: boolean;
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/fastify/createMiddleware.d.ts
|
|
20
|
+
declare const createMiddleware: FastifyMiddlewareFn;
|
|
21
|
+
declare const createRPCMiddleware: FastifyMiddlewareFn;
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/types.d.ts
|
|
24
|
+
type BodyResult = {
|
|
25
|
+
contentType: "application/json";
|
|
26
|
+
data: JsonValue;
|
|
27
|
+
} | {
|
|
28
|
+
contentType: "text/plain";
|
|
29
|
+
data: string;
|
|
30
|
+
};
|
|
31
|
+
// primitives and their compositions
|
|
32
|
+
type JsonPrimitive = string | number | boolean | null | undefined;
|
|
33
|
+
type JsonObject = {
|
|
34
|
+
[key: string]: JsonValue | JsonArray;
|
|
35
|
+
};
|
|
36
|
+
type JsonArray = JsonValue[];
|
|
37
|
+
type JsonValue = JsonPrimitive | JsonArray | JsonObject;
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/fastify/helpers.d.ts
|
|
40
|
+
declare function attachRPC(app: FastifyInstance): Promise<void>;
|
|
41
|
+
declare function attachVite(app: FastifyInstance, vite: ViteDevServer): void;
|
|
42
|
+
declare const readBody: (req: FastifyRequest) => Promise<BodyResult>;
|
|
43
|
+
//#endregion
|
|
44
|
+
export { type FastifyMiddlewareFn, type FastifyMiddlewareHooks, type FastifyMiddlewareOptions, type RpcFastifyPluginOptions, attachRPC, attachVite, createMiddleware, createRPCMiddleware, readBody };
|
|
45
|
+
//# sourceMappingURL=fastify.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fastify.d.mts","names":[],"sources":["../../src/fastify/types.d.ts","../../src/fastify/createMiddleware.ts","../../src/types.d.ts","../../src/fastify/helpers.ts"],"mappings":";;;;;;;;KAOY,2BAA2B;KAE3B,uBACV,UAAU,yCAEV,iBAAiB,QAAQ,8BACtB;UAEY;EACf,UACE,KAAK,gBACL,KAAK,cACL,MAAM,4BACH;;;KAIK,0BAA0B;EACpC;;;;cCLW,kBAAkB;cAyElB,qBAAqB;;;KCxDtB;EACN;EAAiC,MAAM;;EACvC;EAA2B;;;KAOrB;KACA;GAAgB,cAAc,YAAY;;KAC1C,YAAY;KACZ,YAAY,gBAAgB,YAAY;;;iBCzC9B,UAAU,KAAK,kBAAe;iBAMpC,WAAW,KAAK,iBAAiB,MAAM;cAU1C,WAAQ,KACd,mBACJ,QAAQ"}
|