@thednp/rpc 0.0.13 → 0.1.0
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 +16 -14
- package/CHANGELOG.md +246 -0
- package/README.md +27 -8
- package/dist/express/express.d.mts.map +1 -1
- package/dist/express/express.mjs +55 -9
- package/dist/express/express.mjs.map +1 -1
- package/dist/fastify/fastify.d.mts +4 -1
- package/dist/fastify/fastify.d.mts.map +1 -1
- package/dist/fastify/fastify.mjs +51 -7
- package/dist/fastify/fastify.mjs.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.d.mts +14 -0
- package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.mjs +51 -7
- package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
- package/dist/helpers/helpers.d.mts +2 -2
- package/dist/helpers/helpers.d.mts.map +1 -1
- package/dist/helpers/helpers.mjs +1 -1
- package/dist/helpers/helpers.mjs.map +1 -1
- package/dist/hono/hono.d.mts +4 -1
- package/dist/hono/hono.d.mts.map +1 -1
- package/dist/hono/hono.mjs +51 -7
- package/dist/hono/hono.mjs.map +1 -1
- package/dist/index.d.mts +25 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +257 -227
- package/dist/index.mjs.map +1 -1
- package/dist/koa/koa.mjs +51 -7
- package/dist/koa/koa.mjs.map +1 -1
- package/dist/server/server.d.mts +56 -8
- package/dist/server/server.d.mts.map +1 -1
- package/dist/server/server.mjs +124 -29
- package/dist/server/server.mjs.map +1 -1
- package/package.json +11 -9
package/AGENTS.md
CHANGED
|
@@ -17,16 +17,18 @@ pnpm fix:ts # deno lint src --fix
|
|
|
17
17
|
pnpm check:ts # tsc -noEmit
|
|
18
18
|
pnpm format # deno fmt src
|
|
19
19
|
pnpm build # tsdown (outputs to dist/)
|
|
20
|
-
pnpm up:examples # Update all example deps
|
|
21
|
-
pnpm up:
|
|
20
|
+
pnpm up:examples # Update all example deps (to latest published @thednp/rpc + latest example deps)
|
|
21
|
+
pnpm up:examples:lib # Sync examples to the latest published @thednp/rpc version
|
|
22
22
|
pnpm up:root # Update root deps
|
|
23
|
-
pnpm
|
|
24
|
-
pnpm
|
|
23
|
+
pnpm up:deno # deno update + sync deno.json deps
|
|
24
|
+
pnpm upd # Update all deps (up:examples + up:examples:lib + up:root)
|
|
25
|
+
pnpm prepareOnly # upd + up:deno + lint + format + audit:src + build
|
|
26
|
+
pnpm release # Publish npm + jsr (scripts/release.js)
|
|
25
27
|
```
|
|
26
28
|
|
|
27
29
|
## Build Order
|
|
28
30
|
|
|
29
|
-
`lint -> check:ts -> format -> build` (verified in `
|
|
31
|
+
`lint -> check:ts -> format -> build` (verified in `prepareOnly`)
|
|
30
32
|
|
|
31
33
|
## Examples
|
|
32
34
|
|
|
@@ -81,7 +83,7 @@ The tsdown.config.ts produces multiple entries:
|
|
|
81
83
|
|
|
82
84
|
## Important Notes
|
|
83
85
|
|
|
84
|
-
- In dev mode, **only** the Vite dev server and Express
|
|
86
|
+
- In dev mode, **only** the Vite dev server ([Connect](https://github.com/senchalabs/connect) powered) and Express middleware are available, which means adapters don't work in DEV mode
|
|
85
87
|
- Uses `deno` for linting and formatting (not eslint/prettier)
|
|
86
88
|
- Uses `tsdown` for bundling (not rollup/vite directly)
|
|
87
89
|
- Uses `vitest` for testing with `istanbul` coverage
|
|
@@ -131,12 +133,12 @@ The framework's security boundary is the **RPC prefix-gated HTTP endpoint**. Inp
|
|
|
131
133
|
|
|
132
134
|
## Documentation
|
|
133
135
|
|
|
134
|
-
- `wiki/
|
|
135
|
-
- `wiki/getting-started.md` —
|
|
136
|
-
- `wiki/
|
|
137
|
-
- `wiki/server-functions.md` —
|
|
138
|
-
- `wiki/client-usage.md` — Client-side usage
|
|
139
|
-
- `wiki/
|
|
140
|
-
- `wiki/adapters.md` — Framework adapters
|
|
136
|
+
- `wiki/quickstart.md` — Rebuild the Express SSR example from `create-vite` in under a minute (copy-paste)
|
|
137
|
+
- `wiki/getting-started.md` — Installation, project structure, auto-scanning, and your first function
|
|
138
|
+
- `wiki/configuration.md` — Configuration reference (`rpc.config.ts`, `vite.config.ts`, options)
|
|
139
|
+
- `wiki/server-functions.md` — `createServerFunction` API, methods, validation
|
|
140
|
+
- `wiki/client-usage.md` — Client-side usage, type safety, react-query integration
|
|
141
|
+
- `wiki/wire-protocol.md` — HTTP contract, request/response bodies, curl debugging
|
|
142
|
+
- `wiki/adapters.md` — Framework adapters (Express, Fastify, Hono, Koa)
|
|
141
143
|
- `wiki/security.md` — Security hardening
|
|
142
|
-
- `wiki/best-practices.md` —
|
|
144
|
+
- `wiki/best-practices.md` — Production patterns (auth, rate limiting, body limits, CSRF)
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-08-06
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **Typed errors with env-aware responses**: new `RPCError` class (`message` + `code` + optional `data`) and `formatError` helper. All adapters now return `{ error: "Internal Server Error" }` in production (no message/stack leak) and include the error message (plus `code`/`data` for `RPCError`) in development
|
|
8
|
+
- **Duplicate server function detection**: the scan throws in development when two files export functions with the same registered name, so the conflict surfaces at dev-server startup; in production it warns and keeps the first registration
|
|
9
|
+
- **Glob server file scanning**: new `serverFiles: "glob"` option recursively matches `*.server.{ts,js,mjs,mts}` under the scan root, complementing the classic exact `server.ts|js|mjs|mts` names
|
|
10
|
+
- **`scanRoot` option**: point scanning at any directory (relative to the Vite root), e.g. a shared RPC package in a monorepo
|
|
11
|
+
- **`multipart/form-data` content type**: the `contentType` option and `BodyResult` now include multipart; adapters detect multipart bodies from framework parsers (`multer`, `@fastify/multipart`, `koa-body`, Hono form helpers) and pass the parsed fields as the function argument, falling back to `{ raw: <body> }` on the raw stream path
|
|
12
|
+
|
|
13
|
+
### Refactor
|
|
14
|
+
|
|
15
|
+
- `src/index.ts` imports `scanForServerFiles`/`getClientModules`/`serverFunctionsMap` from source modules instead of the `@thednp/rpc/server` self-reference (which resolved to stale `dist/` types during type-checking)
|
|
16
|
+
- Plugin `options` initialized from `defaultRPCOptions` at creation time, so hooks can be invoked without `configResolved` having run
|
|
17
|
+
|
|
18
|
+
### Tests
|
|
19
|
+
|
|
20
|
+
- `formatError` unit tests (production vs development, `RPCError` code/data)
|
|
21
|
+
- Multipart `readBody` tests for all four adapters (pre-parsed and raw stream paths)
|
|
22
|
+
- Scan tests: glob mode (recursive + explicit `scanRoot`), duplicate detection (throw in dev, warn in production)
|
|
23
|
+
|
|
24
|
+
### Chores
|
|
25
|
+
|
|
26
|
+
- Rename the `prepublishOnly` script to `prepareOnly` in `package.json` (keeping `prepublishOnly_` as a non-triggering alias) so `npm publish` inside the release script no longer auto-runs the full pipeline
|
|
27
|
+
- Import `createRPCMiddleware` from the `@thednp/rpc/express` subpath export in `src/index.ts` instead of the relative `./express/createMiddleware.ts`; add `@thednp/rpc/express` to the tsdown externals and the vitest alias map
|
|
28
|
+
- Refactor `getClientModules` to build the generated client modules into a local `entries` const before assembling the output
|
|
29
|
+
- Publish workflow: remove the npm debug-log diagnostic step
|
|
30
|
+
- Sync all 6 examples to `@thednp/rpc ^0.0.13`; bump `@fastify/compress` to `^9.1.1` in the fastify example and add it to `minimumReleaseAgeExclude` in `pnpm-workspace.yaml`
|
|
31
|
+
|
|
32
|
+
## [0.0.13] - 2026-08-04
|
|
33
|
+
|
|
34
|
+
### Chores
|
|
35
|
+
|
|
36
|
+
- Bump version to `0.0.13`
|
|
37
|
+
- Add `hono@4.13.0` and `@hono/node-server@2.1.0` to `minimumReleaseAgeExclude` in `pnpm-workspace.yaml` so pnpm 11's default 1-day minimum release age doesn't hold back the freshly published versions
|
|
38
|
+
- Update dependencies: `hono ^4.13.0`, `@hono/node-server ^2.1.0`, `fastify ^5.11.2`; sync all 6 examples to `@thednp/rpc ^0.0.12`
|
|
39
|
+
- Restore the `prepublishOnly` script name in `package.json`
|
|
40
|
+
|
|
41
|
+
### Docs
|
|
42
|
+
|
|
43
|
+
- Clarify `wiki/client-usage.md` and `wiki/security.md`, `README.md`, and `tsdown.config.ts` comments
|
|
44
|
+
|
|
45
|
+
## [0.0.12] - 2026-08-03
|
|
46
|
+
|
|
47
|
+
### Chores
|
|
48
|
+
|
|
49
|
+
- Bump version to `0.0.12`
|
|
50
|
+
- Publish workflow: drop `--provenance` from the npm publish step to match the vite-style publish flow (npm 11 auto-attaches provenance in trusted-publishing mode; the raw well-formed PUT fallback remains the working publish path)
|
|
51
|
+
- Add `scripts/update-examples.js` — syncs all examples to the latest published `@thednp/rpc` version — wired as `up:examples:lib` and included in `up:examples`
|
|
52
|
+
- Update all 6 examples to `@thednp/rpc ^0.0.11`
|
|
53
|
+
|
|
54
|
+
## [0.0.11] - 2026-08-02
|
|
55
|
+
|
|
56
|
+
### Chores
|
|
57
|
+
|
|
58
|
+
- Bump version to `0.0.11`
|
|
59
|
+
- Publish workflow: use npm only
|
|
60
|
+
|
|
61
|
+
## [0.0.10] - 2026-08-02
|
|
62
|
+
|
|
63
|
+
### Chores
|
|
64
|
+
|
|
65
|
+
- Bump version to `0.0.10`
|
|
66
|
+
- Publish workflow: pin `npm i -g npm@latest` (verified against npm 12.0.2 — OIDC token exchange `201` + sigstore provenance still published, but the `npm publish` PUT remains masked-403), strip the diagnostic capture/bisect steps and the `debug_403` dispatch input, and move `scripts/npm-request-recorder.js` to `experiments/`
|
|
67
|
+
|
|
68
|
+
## [0.0.9] - 2026-08-02
|
|
69
|
+
|
|
70
|
+
### Chores
|
|
71
|
+
|
|
72
|
+
- Bump version to `0.0.9`
|
|
73
|
+
- Publish workflow: drop `--allow-slow-types` from the jsr publish step (verified clean via `deno publish --dry-run`, no slow-types diagnostics)
|
|
74
|
+
|
|
75
|
+
## [0.0.8] - 2026-08-02
|
|
76
|
+
|
|
77
|
+
### Chores
|
|
78
|
+
|
|
79
|
+
- Bump version to `0.0.8`
|
|
80
|
+
- Publish workflow: move the header bisect diagnostic after the raw PUT fallback so probes always replay against the already-published version (never publish a fresh one); add a jsr version guard so release-triggered re-runs are no-ops
|
|
81
|
+
|
|
82
|
+
## [0.0.7] - 2026-08-02
|
|
83
|
+
|
|
84
|
+
### Chores
|
|
85
|
+
|
|
86
|
+
- Bump version to `0.0.7`
|
|
87
|
+
- Publish workflow: attempt plain `npm publish --provenance` (OIDC trusted publishing with provenance enabled) before falling back to the raw well-formed PUT; gate the CLI-403 diagnostic steps (request capture + header bisect) behind a `debug_403` workflow_dispatch input so release runs stay clean
|
|
88
|
+
|
|
89
|
+
## [0.0.6] - 2026-08-02
|
|
90
|
+
|
|
91
|
+
### Docs
|
|
92
|
+
|
|
93
|
+
- Add a **Body Size Limits** section to each framework's section in `wiki/adapters.md` (Express `express.json({ limit })`, Fastify `bodyLimit`, Hono `hono/body-limit`, Koa `koaBody({ jsonLimit })`), each matching its `examples/<framework>/server.js` implementation
|
|
94
|
+
- Clarify in `wiki/security.md` that Koa's official `koa-body` and Hono's built-in `hono/body-limit` middleware are the body size limiting enforcement points
|
|
95
|
+
|
|
96
|
+
### Chores
|
|
97
|
+
|
|
98
|
+
- Bump version to `0.0.6`
|
|
99
|
+
- Add `scripts/npm-request-recorder.js` — an in-process HTTP(S) request/response recorder injected into `npm publish` via `NODE_OPTIONS="--import"` that dumps the exact request headers/body and the server response to diagnose the CLI's masked 403 publish error
|
|
100
|
+
- Publish workflow: skip the CLI publish if the version already exists on the registry, add a "Capture CLI 403 (diagnostic)" step, and move `npm pack` output to `/tmp` so the git tree stays clean
|
|
101
|
+
|
|
102
|
+
## [0.0.5] - 2026-08-02
|
|
103
|
+
|
|
104
|
+
### TypeScript
|
|
105
|
+
|
|
106
|
+
- Export the `InnerModReturn` helper type from `src/types.d.ts` instead of declaring it locally in `src/helpers.ts`, so consumers can reference the `{ data, cancel }` return shape of `innerModule`
|
|
107
|
+
- Move all remaining module-local type/interface definitions into their closest `types.d.ts`, exporting them: `ScanConfig` and `RpcPluginOptionsInternal` → `src/types.d.ts`, `FastifyRPCPlugin`/`FastifyPlugin`/`RegisteredFastifyRPCPlugin` → `src/fastify/types.d.ts`, `IncomingWithBody` → `src/hono/types.d.ts`; removed now-unused imports (`ResolvedConfig`, `FastifyInstance`, `FastifyPlugin`) from their origin files
|
|
108
|
+
|
|
109
|
+
### Docs
|
|
110
|
+
|
|
111
|
+
- Update all 6 examples (`spa`, `express`, `fastify`, `hono`, `koa`, `ssr`) to `@thednp/rpc ^0.0.4`
|
|
112
|
+
- Clarify in the README "Why this exists" section the niche `@thednp/rpc` targets: RPC without the weight of an entire framework (Vite sites, static SPAs, single-middleware servers) — no meta-framework, full-stack router, or vendor required
|
|
113
|
+
|
|
114
|
+
### Chores
|
|
115
|
+
|
|
116
|
+
- Bump version to `0.0.5`
|
|
117
|
+
- `scripts/dev-test.js`: the default `@thednp/rpc` version used when restoring example deps is now read from the root `package.json` (as `^<version>`) instead of removing the dependency when no original value was saved
|
|
118
|
+
- Rebuild `dist/` to pick up the exported types
|
|
119
|
+
|
|
120
|
+
## [0.0.4] - 2026-08-02
|
|
121
|
+
|
|
122
|
+
### Breaking
|
|
123
|
+
|
|
124
|
+
- Restrict RPC dispatch to configured HTTP methods: server functions default to `POST` and are now rejected with `405 Method Not Allowed` on any other method (previously any method was accepted)
|
|
125
|
+
|
|
126
|
+
### Features
|
|
127
|
+
|
|
128
|
+
- Add `method` option to `ServerFunctionOptions` (`"GET" | "POST"`, default `"POST"`) for per-function HTTP method control
|
|
129
|
+
- Add `origin` option to `MiddlewareOptions` — when set, requests with a mismatching `Origin` header are rejected with `403 Forbidden` (requests without an `Origin` header, e.g. curl, pass)
|
|
130
|
+
- GET function dispatch: generated client modules send args as a URL-encoded `?args=` JSON query parameter (no request body)
|
|
131
|
+
|
|
132
|
+
### Security
|
|
133
|
+
|
|
134
|
+
- Enforce method + origin checks in all 4 adapters (Express, Fastify, Hono, Koa) with anchored prefix matching already in place
|
|
135
|
+
- Hono adapter: guard `env.incoming` with optional chaining so bare serverless environments without an incoming stream no longer crash
|
|
136
|
+
|
|
137
|
+
### Fixes
|
|
138
|
+
|
|
139
|
+
- Scan server files by exact filename (`.ts`/`.mjs`/`.cjs` in the configured directory) instead of substring matching, so files like `server.tsx` are no longer picked up
|
|
140
|
+
- Bump `@hono/node-server` to `^2.0.5` via `pnpm-workspace.yaml` override (fixes GHSA-frvp-7c67-39w9 audit advisory pulled in transitively by `@hono/vite-dev-server`)
|
|
141
|
+
|
|
142
|
+
### Docs
|
|
143
|
+
|
|
144
|
+
- Add `wiki/best-practices.md` sections on rate limiting and Origin/CSRF protection with framework-specific middleware snippets
|
|
145
|
+
- Document the `method` option in `wiki/server-functions.md`
|
|
146
|
+
- Add Method Enforcement and Origin Validation sections to `wiki/security.md`
|
|
147
|
+
- Document exact scan filename matching in `wiki/setup.md`
|
|
148
|
+
- Add an "HTTP Method" section to `wiki/server-functions.md` explaining why only GET and POST are supported (RPC has no resource semantics, OPTIONS is reserved for CORS preflight, minimal attack surface)
|
|
149
|
+
- Add a method-restriction security note (GET/POST only) to the README security section
|
|
150
|
+
|
|
151
|
+
### Chores
|
|
152
|
+
|
|
153
|
+
- Bump version to `0.0.4`
|
|
154
|
+
- Add test coverage for method dispatch (POST default, GET with `?args=`, 405 enforcement), origin validation (mismatch 403, absent origin passes), and exact scan matching
|
|
155
|
+
- Reach 100% test coverage (232 tests): add `validateMethod` suite, GET client-module fetch test, and GET dispatch coverage for Hono and Koa
|
|
156
|
+
- Add a GET server function demo (`getServerTime`) with a "Get time" UI and shareable link to all 6 examples; add `tsconfig.json` to the `ssr` example and `types: ["vite/client"]` to example tsconfigs
|
|
157
|
+
- Move `dev-test.js` to `scripts/dev-test.js` and enhance it: switch examples to `link:../..` before testing and restore the published version afterwards, verify GET dispatch, install with `--no-frozen-lockfile`
|
|
158
|
+
- Add `scripts/audit-src.js` — audits only the root package's dependencies in a temp project (344 deps vs 413 for the full workspace) — wired into `prepublishOnly` as `audit:src`
|
|
159
|
+
- Add `scripts/update-deno.js` to sync JSR metadata (`version`, `description`, `keywords`, `license`) from `package.json` into `deno.json`; add `up:deno` task
|
|
160
|
+
- Refactor `deno.json` tasks to `deno task` self-references; `prepublishOnly` is now `upd` + `lint` + `format` + `audit:src` + `build`
|
|
161
|
+
- Run `pnpm audit` in CI and trigger workflows on `pnpm-workspace.yaml` changes
|
|
162
|
+
- Sync `deno.json` keywords with `package.json` (`vite`, `vite-plugin` added)
|
|
163
|
+
|
|
164
|
+
## [0.0.3] - 2026-07-30
|
|
165
|
+
|
|
166
|
+
### Docs
|
|
167
|
+
|
|
168
|
+
- Add comprehensive JSDoc to all exported functions across the codebase (~50 symbols) with `@param` and `@returns` tags
|
|
169
|
+
- Add `@module` JSDoc to all 8 entrypoints for JSR documentation generation
|
|
170
|
+
- Add JSDoc to all exported types, interfaces, and their properties (framework hooks, middleware options, JSON types, adapter types) for JSR documentation scoring
|
|
171
|
+
|
|
172
|
+
### Chores
|
|
173
|
+
|
|
174
|
+
- Add `description`, `author`, and `keywords` to `deno.json` for JSR metadata
|
|
175
|
+
- Add `imports` map to `deno.json` for JSR self-referencing resolution
|
|
176
|
+
- Bump version to `0.0.3`
|
|
177
|
+
|
|
178
|
+
## [0.0.2] - 2026-07-30
|
|
179
|
+
|
|
180
|
+
### Breaking
|
|
181
|
+
|
|
182
|
+
- Rename `rpcPreffix` → `rpcPrefix` across all configs, adapters, types, and docs (the old typo is no longer recognized)
|
|
183
|
+
|
|
184
|
+
### Features
|
|
185
|
+
|
|
186
|
+
- Add `credentials` option to `ServerFunctionOptions` (`"same-origin" | "include" | "omit"`, default `"same-origin"`)
|
|
187
|
+
- Add `validateCredentials()` to validate the credentials option at build time
|
|
188
|
+
- Rename `rpcPreffix` internal variable consistently to `rpcPrefix`
|
|
189
|
+
- Thread `credentials` through generated client modules and `innerModule`
|
|
190
|
+
|
|
191
|
+
### Refactor
|
|
192
|
+
|
|
193
|
+
- Centralize all error/warning messages into `src/constants.ts`
|
|
194
|
+
- Extract validation functions (`validateIdentifier`, `validatePathSegment`) into `src/validate.ts` with 100% test coverage
|
|
195
|
+
- Move safe-identifier regex patterns from `constants.ts` to `validate.ts` (implementation detail)
|
|
196
|
+
- Add explicit return types to all adapter helpers and core functions
|
|
197
|
+
- Add `RequestDetails` and `ResponseDetails` types to Express adapter
|
|
198
|
+
- Add `InnerModReturn` helper type to `helpers.ts`
|
|
199
|
+
- Fully type Fastify plugin with explicit interface
|
|
200
|
+
- Type `serverFunctionsMap` explicitly in `functionsMap.ts`
|
|
201
|
+
- Convert `defineConfig` and `loadRPCConfig` to typed arrow functions
|
|
202
|
+
|
|
203
|
+
### Fixes
|
|
204
|
+
|
|
205
|
+
- Publish workflow: remove `--provenance`, Node version 24
|
|
206
|
+
- Fix AGENTS.md express command, fix Koa adapter docs
|
|
207
|
+
- README GitHub URLs: `rpcv` → `rpc`
|
|
208
|
+
- Proper pnpm workspace setup for StackBlitz
|
|
209
|
+
- StackBlitz compatibility for examples
|
|
210
|
+
- Remove `prepare` script to prevent rolldown native binding error on StackBlitz
|
|
211
|
+
- SPA proxy server: cast `createRPCMiddleware` options for type safety
|
|
212
|
+
- Move adapter deps from `devDependencies` to `dependencies` for correct runtime resolution
|
|
213
|
+
|
|
214
|
+
### Docs
|
|
215
|
+
|
|
216
|
+
- Expand Authentication section in best-practices with Basic Authorization + Per-Function Authorization examples
|
|
217
|
+
- Add CONTRIBUTING section to README
|
|
218
|
+
- Update all wiki docs to use `rpcPrefix`
|
|
219
|
+
- Clarify isomorphic nature of server functions in wiki
|
|
220
|
+
- Add README.md to each example
|
|
221
|
+
- Use full wiki URLs in example READMEs
|
|
222
|
+
- Fix broken link in security.md (`best-practives.md` → `best-practices.md`)
|
|
223
|
+
- Fix cancellation description in client-usage.md to match actual behavior
|
|
224
|
+
|
|
225
|
+
### Chores
|
|
226
|
+
|
|
227
|
+
- Create CHANGELOG.md
|
|
228
|
+
- Update `dist/` with latest builds
|
|
229
|
+
- Update examples dependencies
|
|
230
|
+
- Bump version to `0.0.2`
|
|
231
|
+
- Add `keywords` field to package.json
|
|
232
|
+
- Move `picocolors` from root to examples/spa
|
|
233
|
+
|
|
234
|
+
## [0.0.1] - 2026-07-28
|
|
235
|
+
|
|
236
|
+
### Initial Release
|
|
237
|
+
|
|
238
|
+
- Vite plugin for automatic RPC generation
|
|
239
|
+
- Framework-agnostic core with adapters for Express, Fastify, Hono, and Koa
|
|
240
|
+
- Auto-scanning of server functions via `scanForServerFiles`
|
|
241
|
+
- Client-side module generation at build time
|
|
242
|
+
- Request cancellation via `AbortController`
|
|
243
|
+
- Prefix-gated RPC endpoint with regex boundary protection
|
|
244
|
+
- Code injection prevention in client module generation
|
|
245
|
+
- SSR and SPA support
|
|
246
|
+
- Configuration via `rpc.config.ts`
|
package/README.md
CHANGED
|
@@ -16,9 +16,9 @@ The server functions run **isomorphically** within any Vite powered runtime.
|
|
|
16
16
|
|
|
17
17
|
## Why this exists
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Many RPC solutions like to overcomplicate things to the point where you no longer ship features, you're maintaining a framework. RPC should be a bridge, not a metropolis.
|
|
20
20
|
|
|
21
|
-
`@thednp/rpc`
|
|
21
|
+
`@thednp/rpc` allows you to supercharge any vite powered SPA/SSR starter template in minutes. To prove it, we made a quick guide to [recreate our Express SSR example](./wiki/quickstart.md).
|
|
22
22
|
|
|
23
23
|
### Simplicity is best
|
|
24
24
|
|
|
@@ -26,7 +26,7 @@ Most RPC solutions ask you to adopt a new way of thinking, require learning a co
|
|
|
26
26
|
<details>
|
|
27
27
|
<summary><b>Server functions should just be functions</b></summary>
|
|
28
28
|
|
|
29
|
-
You define them in a file, import and call them where you need them. The plugin handles everything in between — system wide configuration, scanning, type inference, client
|
|
29
|
+
You define them in a file, import and call them where you need them. The plugin handles everything in between — system wide configuration, scanning, type inference, client fetch modules generation, middleware registration, request cancellation — without asking you to restructure your codebase.
|
|
30
30
|
</details>
|
|
31
31
|
|
|
32
32
|
<details>
|
|
@@ -34,7 +34,8 @@ You define them in a file, import and call them where you need them. The plugin
|
|
|
34
34
|
|
|
35
35
|
* `createFunction.ts` — server-side definition (wrapped handler with `AbortController`)
|
|
36
36
|
* `getClientModules.ts` — build-time code generation (string template with validation)
|
|
37
|
-
* `helpers.ts` — client-side runtime (thin `fetch` based modules)
|
|
37
|
+
* `client-helpers.ts` — client-side runtime (thin `fetch` based modules)
|
|
38
|
+
* `server-helpers.ts` — server-only utilities (`RPCError`, error formatting, glob file walking)
|
|
38
39
|
* `scanForServerFiles.ts` — file discovery
|
|
39
40
|
* **Adapters** — thin middleware wrappers
|
|
40
41
|
</details>
|
|
@@ -80,6 +81,18 @@ Every server function call returns a handle with a `cancel()` helper. Under the
|
|
|
80
81
|
The core plugin doesn't care whether you're running Express, Fastify, Hono, or Koa. Adapters for all four are bundled with the package — you import the one you need, register it as middleware, and you're done. If you're building a plain SPA with no server framework at all, the Vite dev server handles RPC requests directly in development. No adapter needed.
|
|
81
82
|
</details>
|
|
82
83
|
|
|
84
|
+
<details>
|
|
85
|
+
<summary><b>Flexible server file discovery</b></summary>
|
|
86
|
+
|
|
87
|
+
Scan `src/api/` for classic `server.ts|js|mjs|mts` files, or switch to glob mode (`serverFiles: 'glob'`) to recursively pick up `*.server.{ts,js,mjs,mts}` files — handy for feature-based layouts. A `scanRoot` option points scanning at a shared package directory in monorepos. Duplicate function names throw in development so the conflict is fixed immediately (warning in production).
|
|
88
|
+
</details>
|
|
89
|
+
|
|
90
|
+
<details>
|
|
91
|
+
<summary><b>Typed errors, safe by default</b></summary>
|
|
92
|
+
|
|
93
|
+
Server errors return a generic `Internal Server Error` in production — no messages, codes, or stacks leak to clients. In development the message (and `code`/`data` for `RPCError`) is included so you can debug instantly. `multipart/form-data` content type is supported for file uploads via your framework's multipart parser.
|
|
94
|
+
</details>
|
|
95
|
+
|
|
83
96
|
<details>
|
|
84
97
|
<summary><b>TypeScript throughout</b></summary>
|
|
85
98
|
|
|
@@ -170,7 +183,7 @@ Check [Configuration Guide](wiki/configuration.md) for details.
|
|
|
170
183
|
Create `src/api/server.ts`:
|
|
171
184
|
|
|
172
185
|
```ts
|
|
173
|
-
import { createServerFunction } from "@thednp/rpc/server";
|
|
186
|
+
import { createServerFunction, RPCError } from "@thednp/rpc/server";
|
|
174
187
|
|
|
175
188
|
export const greet = createServerFunction("greet", (signal, name: string) => {
|
|
176
189
|
// access AbortSignal
|
|
@@ -178,11 +191,16 @@ export const greet = createServerFunction("greet", (signal, name: string) => {
|
|
|
178
191
|
|
|
179
192
|
// add validation and other server ONLY functionality
|
|
180
193
|
|
|
194
|
+
// throw typed errors for server-side failures
|
|
195
|
+
if (!name) throw new RPCError("Name is required", "EMPTY_NAME");
|
|
196
|
+
|
|
181
197
|
// return the result of processing
|
|
182
198
|
return `Hello, ${name}!`;
|
|
183
199
|
});
|
|
184
200
|
```
|
|
185
201
|
|
|
202
|
+
`RPCError` is the typed error helper — in development its message (and `code`/`data`) reach the client for instant debugging; in production the response is always a generic `Internal Server Error`.
|
|
203
|
+
|
|
186
204
|
Create `src/api/index.ts`:
|
|
187
205
|
|
|
188
206
|
```ts
|
|
@@ -231,7 +249,7 @@ pnpm test-ui # Run tests with UI
|
|
|
231
249
|
pnpm test --run # Single run
|
|
232
250
|
```
|
|
233
251
|
|
|
234
|
-
Tests use **Vitest** with **Istanbul** coverage
|
|
252
|
+
Tests use **Vitest** with **Istanbul** coverage — 8 test files covering the plugin, scanning, client/server helpers, and all four adapters, at 100% coverage.
|
|
235
253
|
|
|
236
254
|
### Live Testing
|
|
237
255
|
|
|
@@ -307,11 +325,12 @@ The full threat model, including edge cases and configuration options for tighte
|
|
|
307
325
|
|
|
308
326
|
## Documentation
|
|
309
327
|
|
|
310
|
-
- [
|
|
311
|
-
- [
|
|
328
|
+
- [Quick Start](./wiki/quickstart.md) — Rebuild the Express SSR example from `create-vite` in under a minute
|
|
329
|
+
- [Getting Started](./wiki/getting-started.md) — Installation, project structure, and your first function
|
|
312
330
|
- [Configuration](./wiki/configuration.md)
|
|
313
331
|
- [Server Functions](./wiki/server-functions.md)
|
|
314
332
|
- [Client Usage](./wiki/client-usage.md)
|
|
333
|
+
- [Wire Protocol](./wiki/wire-protocol.md) — The HTTP contract behind the generated clients (curl debugging)
|
|
315
334
|
- [Adapters](./wiki/adapters.md)
|
|
316
335
|
- [Best Practices](./wiki/best-practices.md)
|
|
317
336
|
- [Security](./wiki/security.md)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.d.mts","names":[],"sources":["../../src/express/types.d.ts","../../src/express/createMiddleware.ts","../../src/express/helpers.ts"],"mappings":";;;;;;;;KAgBY,2BAA2B;;;;;KAM3B,uBACV,UAAU,yCAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiB,UACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;KAOK;;EAEV;;EAEA,YAAY,cAAc;;EAE1B;;EAEA,gBAAgB;;EAEhB,eAAe,cAAc,QAAQ,eAAe;;;;;KAM1C;;EAEV;;EAEA;;EAEA,cAAc;;EAEd,SAAS;;EAET;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"express.d.mts","names":[],"sources":["../../src/express/types.d.ts","../../src/express/createMiddleware.ts","../../src/express/helpers.ts"],"mappings":";;;;;;;;KAgBY,2BAA2B;;;;;KAM3B,uBACV,UAAU,yCAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiB,UACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;KAOK;;EAEV;;EAEA,YAAY,cAAc;;EAE1B;;EAEA,gBAAgB;;EAEhB,eAAe,cAAc,QAAQ,eAAe;;;;;KAM1C;;EAEV;;EAEA;;EAEA,cAAc;;EAEd,SAAS;;EAET;;;;;;;;;;;cCvCW,kBAAkB;;;;;;;;cAyElB,qBAAqB;;;;;;;;iBC3FZ,UAAU,KAAK,UAAO;;;;;;iBAW5B,WAAW,KAAK,SAAS,MAAM;;;;;;;;cAWlC,WAAQ,KACd,UAAiB,oBACrB,QAAQ;;;;;;cAyEE,mBAAgB,KACtB,kBAAkB,YACtB,OAAO;;;;;;cASG,oBAAiB,KACvB,iBAAiB,aACrB,OAAO;;;;;;;cAUG,mBAAgB,KACtB,kBAAkB,YACtB,OAAO;;;;;;;cAUG,oBAAiB,SACnB,UAAiB,oBACzB;;;;;;;cAqBU,qBAAkB,UACnB,WAAkB,mBAC3B"}
|
package/dist/express/express.mjs
CHANGED
|
@@ -2,7 +2,9 @@ import { scanForServerFiles, serverFunctionsMap } from "@thednp/rpc/server";
|
|
|
2
2
|
//#region src/options.ts
|
|
3
3
|
const defaultRPCOptions = {
|
|
4
4
|
rpcPrefix: "__rpc",
|
|
5
|
-
adapter: "express"
|
|
5
|
+
adapter: "express",
|
|
6
|
+
serverFiles: "exact",
|
|
7
|
+
scanRoot: void 0
|
|
6
8
|
};
|
|
7
9
|
const defaultMiddlewareOptions = {
|
|
8
10
|
rpcPrefix: void 0,
|
|
@@ -39,10 +41,12 @@ function attachVite(app, vite) {
|
|
|
39
41
|
const readBody = (req) => {
|
|
40
42
|
return new Promise((resolve, reject) => {
|
|
41
43
|
if (hasPreParsedBody(req) && req.body !== void 0) {
|
|
42
|
-
const
|
|
44
|
+
const contentType = req.headers["content-type"]?.toLowerCase() || "";
|
|
45
|
+
const isJSON = contentType.includes("json");
|
|
46
|
+
const isMultipart = contentType.includes("multipart/form-data");
|
|
43
47
|
resolve({
|
|
44
|
-
contentType: isJSON ? "application/json" : "text/plain",
|
|
45
|
-
data: isJSON ? req.body : String(req.body)
|
|
48
|
+
contentType: isMultipart ? "multipart/form-data" : isJSON ? "application/json" : "text/plain",
|
|
49
|
+
data: isMultipart ? req.body : isJSON ? req.body : String(req.body)
|
|
46
50
|
});
|
|
47
51
|
return;
|
|
48
52
|
}
|
|
@@ -58,12 +62,14 @@ const readBody = (req) => {
|
|
|
58
62
|
};
|
|
59
63
|
const onEnd = () => {
|
|
60
64
|
toggleListeners();
|
|
61
|
-
const
|
|
65
|
+
const incomingType = req.headers["content-type"]?.toLowerCase() || "";
|
|
66
|
+
const isJSON = incomingType.includes("json");
|
|
67
|
+
const isMultipart = incomingType.includes("multipart/form-data");
|
|
62
68
|
try {
|
|
63
|
-
const data = JSON.parse(body);
|
|
69
|
+
const data = isMultipart ? { raw: body } : JSON.parse(body);
|
|
64
70
|
resolve({
|
|
65
|
-
contentType: isJSON ? "application/json" : "text/plain",
|
|
66
|
-
data
|
|
71
|
+
contentType: isMultipart ? "multipart/form-data" : isJSON ? "application/json" : "text/plain",
|
|
72
|
+
data: isMultipart ? data : data
|
|
67
73
|
});
|
|
68
74
|
} catch (_e) {
|
|
69
75
|
resolve({
|
|
@@ -173,6 +179,45 @@ const CLIENT_DISCONNECTED = "client disconnected";
|
|
|
173
179
|
/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */
|
|
174
180
|
const MIDDLEWARE_NAME_USED = (name) => `The middleware name "${name}" is already used.`;
|
|
175
181
|
//#endregion
|
|
182
|
+
//#region src/server-helpers.ts
|
|
183
|
+
/**
|
|
184
|
+
* A typed error thrown from server functions.
|
|
185
|
+
* The middleware serializes the `message` and `code` in the response,
|
|
186
|
+
* allowing clients to recognise and handle specific error conditions.
|
|
187
|
+
*/
|
|
188
|
+
var RPCError = class extends Error {
|
|
189
|
+
/** Machine-readable error code (e.g. "VALIDATION_FAILED", "UNAUTHORIZED") */
|
|
190
|
+
code;
|
|
191
|
+
/** Optional diagnostic payload */
|
|
192
|
+
data;
|
|
193
|
+
constructor(message, code = "INTERNAL", data) {
|
|
194
|
+
super(message);
|
|
195
|
+
this.name = "RPCError";
|
|
196
|
+
this.code = code;
|
|
197
|
+
this.data = data;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* Formats an error for the RCP middleware response.
|
|
202
|
+
* In development the full message and stack are included so developers
|
|
203
|
+
* can quickly identify issues. In production only the generic
|
|
204
|
+
* "Internal Server Error" is sent, preventing information disclosure.
|
|
205
|
+
*/
|
|
206
|
+
const formatError = (err, isProduction) => {
|
|
207
|
+
if (!isProduction) {
|
|
208
|
+
if (err instanceof RPCError) {
|
|
209
|
+
const payload = {
|
|
210
|
+
error: err.message || "Internal Server Error",
|
|
211
|
+
code: err.code
|
|
212
|
+
};
|
|
213
|
+
if (err.data !== void 0) payload.data = err.data;
|
|
214
|
+
return payload;
|
|
215
|
+
}
|
|
216
|
+
return { error: (err instanceof Error ? err.message : String(err)) || "Internal Server Error" };
|
|
217
|
+
}
|
|
218
|
+
return { error: INTERNAL_SERVER_ERROR };
|
|
219
|
+
};
|
|
220
|
+
//#endregion
|
|
176
221
|
//#region src/express/createMiddleware.ts
|
|
177
222
|
let middlewareCount = 0;
|
|
178
223
|
const middlewareStack = /* @__PURE__ */ new Set();
|
|
@@ -261,7 +306,8 @@ const createRPCMiddleware = (initialOptions = {}) => {
|
|
|
261
306
|
if (!res.headersSent) sendResponse(200, { data: result });
|
|
262
307
|
} catch (err) {
|
|
263
308
|
console.error(String(err));
|
|
264
|
-
|
|
309
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
310
|
+
sendResponse(500, formatError(err, isProduction));
|
|
265
311
|
}
|
|
266
312
|
}
|
|
267
313
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.mjs","names":[],"sources":["../../src/options.ts","../../src/express/helpers.ts","../../src/tools.ts","../../src/constants.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 credentials: \"same-origin\",\n method: \"POST\",\n} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: \"__rpc\",\n adapter: \"express\",\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: 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 type { Buffer } from \"node:buffer\";\nimport type { ViteDevServer } from \"vite\";\nimport type { Express } from \"express\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\nimport type { RequestDetails, ResponseDetails } from \"./types.d.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to an Express app.\n * Dynamically imports loadRPCConfig and creates the middleware with loaded options.\n * @param app - Express application instance\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\n/**\n * Attaches Vite's dev server middlewares to an Express app for development mode.\n * @param app - Express application instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: Express, vite: ViteDevServer) {\n app.use(vite.middlewares);\n}\n\n/**\n * Reads and parses the HTTP request body from an Express or Node IncomingMessage.\n * If a body parser middleware (e.g. express.json()) already consumed the stream,\n * uses the pre-parsed body from `req.body`.\n * @param req - Express or Node.js IncomingMessage\n * @returns A promise resolving to the parsed body with its content type\n */\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\n/**\n * Type guard that checks whether a request is an Express Request (has `originalUrl`).\n * @param req - A Node IncomingMessage or Express Request\n * @returns True if the request is an Express Request\n */\nexport const isExpressRequest = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"originalUrl\" in req;\n};\n\n/**\n * Type guard that checks whether a response is an Express Response (has `json` and `send` methods).\n * @param res - A Node ServerResponse or Express Response\n * @returns True if the response is an Express Response\n */\nexport const isExpressResponse = (\n res: ServerResponse | ExpressResponse,\n): res is ExpressResponse => {\n return \"json\" in res && \"send\" in res;\n};\n\n/**\n * Type guard that checks whether a request has a pre-parsed body (`body` property).\n * Used to detect if a body-parser middleware already consumed the stream.\n * @param req - A Node IncomingMessage or Express Request\n * @returns True if the request has a body property\n */\nexport const hasPreParsedBody = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"body\" in req;\n};\n\n/**\n * Extracts normalized request details from an Express or Node IncomingMessage.\n * Parses the URL to extract pathname, search string, and search params.\n * @param request - Express or Node.js request object\n * @returns Normalized request details including URL, headers, and method\n */\nexport const getRequestDetails = (\n request: ExpressRequest | IncomingMessage,\n): RequestDetails => {\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\n/**\n * Wraps an Express or Node ServerResponse with a uniform API for setting headers,\n * status codes, and sending JSON responses. Handles the Express vs raw Node API differences.\n * @param response - Express or Node.js server response object\n * @returns A ResponseDetails object with setHeader, setStatusCode, and sendResponse helpers\n */\nexport const getResponseDetails = (\n response: ExpressResponse | ServerResponse,\n): ResponseDetails => {\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","/**\n * Escapes special regex metacharacters in a string.\n * Used to safely embed user-configurable values (like rpcPrefix) into regular expressions,\n * preventing ReDoS and regex injection attacks.\n * @param s - The raw string to escape\n * @returns The escaped string safe for use in new RegExp()\n */\nexport function escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n","export const OPERATION_ABORTED = \"Operation aborted\";\n\nexport const REQUEST_CANCELLED = \"Request was cancelled\";\n\nexport const FETCH_ERROR_PREFIX = \"Fetch error: \";\n\nexport const NO_SERVER_FUNCTION_FOUND = \"No server function found.\";\n\nexport const ERROR_LOADING_FILE = \"Error loading file:\";\n\nexport const FUNCTION_NOT_FOUND = \"Function not found\";\n\nexport const METHOD_NOT_ALLOWED = \"Method Not Allowed\";\n\nexport const REQUEST_FORBIDDEN = \"Forbidden\";\n\nexport const INTERNAL_SERVER_ERROR = \"Internal Server Error\";\n\nexport const CLIENT_DISCONNECTED = \"client disconnected\";\n\n/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */\nexport const MIDDLEWARE_NAME_USED = (name: string) =>\n `The middleware name \"${name}\" is already used.`;\n\n/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */\nexport const INVALID_IDENTIFIER = (label: string, name: string) =>\n `Invalid ${label}: \"${name}\" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;\n\n/** Error message when a value fails the safe-path-segment validation. @param label - What kind of value was being validated. @param segment - The rejected value */\nexport const INVALID_PATH_SEGMENT = (label: string, segment: string) =>\n `Invalid ${label}: \"${segment}\" must match /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/`;\n\n/** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */\nexport const CONFIG_FILE_NOT_FOUND = (\n configFile: string,\n configFilePath: string,\n) =>\n ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;\n\nexport const NO_CONFIG_FOUND = ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\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 type { JsonValue } from \"../types.d.ts\";\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\";\nimport {\n CLIENT_DISCONNECTED,\n FUNCTION_NOT_FOUND,\n INTERNAL_SERVER_ERROR,\n METHOD_NOT_ALLOWED,\n MIDDLEWARE_NAME_USED,\n REQUEST_FORBIDDEN,\n} from \"../constants.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates an Express middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated — reusing a name throws an error.\n * Prefix and path regexes are compiled once at creation time (hoisted) for performance.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns An Express middleware function\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 rpcPrefix = options.rpcPrefix;\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(MIDDLEWARE_NAME_USED(name));\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 = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\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 // rpcPrefix 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/**\n * Creates the Express RPC middleware that routes incoming requests to registered server functions.\n * Reads the request body, dispatches to the matching function via serverFunctionsMap,\n * and sends the JSON-serialized result. Handles client disconnection via abort signals.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns An Express middleware function\n */\nexport const createRPCMiddleware: ExpressMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\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 rpcPrefix = options.rpcPrefix;\n const prefixRegex = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${rpcPrefix}/`;\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: path, searchParams } = 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(path)) {\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 // Optional origin check: reject requests whose Origin header does not\n // match the configured origin. Requests without an Origin header\n // (curl, native clients) pass through unchecked.\n const origin = options.origin;\n const requestOrigin = req.headers.origin;\n if (origin && requestOrigin && requestOrigin !== origin) {\n sendResponse(403, { error: REQUEST_FORBIDDEN });\n return;\n }\n\n const functionName = path.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 method = serverFunction.options?.method || \"POST\";\n if (req.method?.toUpperCase() !== method) {\n sendResponse(405, { error: METHOD_NOT_ALLOWED });\n return;\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = searchParams.get(\"args\");\n // istanbul ignore else\n if (raw) args = JSON.parse(raw);\n } else {\n const body = await readBody(req);\n args = Array.isArray(body.data) ? body.data : [body.data];\n }\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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"}
|
|
1
|
+
{"version":3,"file":"express.mjs","names":[],"sources":["../../src/options.ts","../../src/express/helpers.ts","../../src/tools.ts","../../src/constants.ts","../../src/server-helpers.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 credentials: \"same-origin\",\n method: \"POST\",\n} satisfies ServerFunctionOptions;\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: \"__rpc\",\n adapter: \"express\",\n serverFiles: \"exact\",\n scanRoot: undefined,\n};\n\nexport const defaultMiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: 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 type { Buffer } from \"node:buffer\";\nimport type { ViteDevServer } from \"vite\";\nimport type { Express } from \"express\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\nimport type { RequestDetails, ResponseDetails } from \"./types.d.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to an Express app.\n * Dynamically imports loadRPCConfig and creates the middleware with loaded options.\n * @param app - Express application instance\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\n/**\n * Attaches Vite's dev server middlewares to an Express app for development mode.\n * @param app - Express application instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: Express, vite: ViteDevServer) {\n app.use(vite.middlewares);\n}\n\n/**\n * Reads and parses the HTTP request body from an Express or Node IncomingMessage.\n * If a body parser middleware (e.g. express.json()) already consumed the stream,\n * uses the pre-parsed body from `req.body`.\n * @param req - Express or Node.js IncomingMessage\n * @returns A promise resolving to the parsed body with its content type\n */\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 const isMultipart = contentType.includes(\"multipart/form-data\");\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : \"text/plain\",\n data: isMultipart\n ? (req.body as Record<string, unknown>)\n : isJSON\n ? req.body\n : 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 const isMultipart = incomingType.includes(\"multipart/form-data\");\n try {\n const data = isMultipart ? { raw: body } : JSON.parse(body);\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : \"text/plain\",\n data: isMultipart ? (data as Record<string, unknown>) : data,\n } as BodyResult);\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\n/**\n * Type guard that checks whether a request is an Express Request (has `originalUrl`).\n * @param req - A Node IncomingMessage or Express Request\n * @returns True if the request is an Express Request\n */\nexport const isExpressRequest = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"originalUrl\" in req;\n};\n\n/**\n * Type guard that checks whether a response is an Express Response (has `json` and `send` methods).\n * @param res - A Node ServerResponse or Express Response\n * @returns True if the response is an Express Response\n */\nexport const isExpressResponse = (\n res: ServerResponse | ExpressResponse,\n): res is ExpressResponse => {\n return \"json\" in res && \"send\" in res;\n};\n\n/**\n * Type guard that checks whether a request has a pre-parsed body (`body` property).\n * Used to detect if a body-parser middleware already consumed the stream.\n * @param req - A Node IncomingMessage or Express Request\n * @returns True if the request has a body property\n */\nexport const hasPreParsedBody = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"body\" in req;\n};\n\n/**\n * Extracts normalized request details from an Express or Node IncomingMessage.\n * Parses the URL to extract pathname, search string, and search params.\n * @param request - Express or Node.js request object\n * @returns Normalized request details including URL, headers, and method\n */\nexport const getRequestDetails = (\n request: ExpressRequest | IncomingMessage,\n): RequestDetails => {\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\n/**\n * Wraps an Express or Node ServerResponse with a uniform API for setting headers,\n * status codes, and sending JSON responses. Handles the Express vs raw Node API differences.\n * @param response - Express or Node.js server response object\n * @returns A ResponseDetails object with setHeader, setStatusCode, and sendResponse helpers\n */\nexport const getResponseDetails = (\n response: ExpressResponse | ServerResponse,\n): ResponseDetails => {\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","/**\n * Escapes special regex metacharacters in a string.\n * Used to safely embed user-configurable values (like rpcPrefix) into regular expressions,\n * preventing ReDoS and regex injection attacks.\n * @param s - The raw string to escape\n * @returns The escaped string safe for use in new RegExp()\n */\nexport function escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n","export const OPERATION_ABORTED = \"Operation aborted\";\n\nexport const REQUEST_CANCELLED = \"Request was cancelled\";\n\nexport const FETCH_ERROR_PREFIX = \"Fetch error: \";\n\nexport const NO_SERVER_FUNCTION_FOUND = \"No server function found.\";\n\nexport const ERROR_LOADING_FILE = \"Error loading file:\";\n\nexport const FUNCTION_NOT_FOUND = \"Function not found\";\n\nexport const METHOD_NOT_ALLOWED = \"Method Not Allowed\";\n\nexport const REQUEST_FORBIDDEN = \"Forbidden\";\n\nexport const INTERNAL_SERVER_ERROR = \"Internal Server Error\";\n\nexport const CLIENT_DISCONNECTED = \"client disconnected\";\n\n/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */\nexport const MIDDLEWARE_NAME_USED = (name: string) =>\n `The middleware name \"${name}\" is already used.`;\n\n/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */\nexport const INVALID_IDENTIFIER = (label: string, name: string) =>\n `Invalid ${label}: \"${name}\" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;\n\n/** Error message when a value fails the safe-path-segment validation. @param label - What kind of value was being validated. @param segment - The rejected value */\nexport const INVALID_PATH_SEGMENT = (label: string, segment: string) =>\n `Invalid ${label}: \"${segment}\" must match /^[A-Za-z0-9_$][A-Za-z0-9_$/-]*$/`;\n\n/** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */\nexport const CONFIG_FILE_NOT_FOUND = (\n configFile: string,\n configFilePath: string,\n) =>\n ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;\n\nexport const NO_CONFIG_FOUND = ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n\n/** Error template for duplicate server function names across files. @param name - The duplicate registered name */\nexport const DUPLICATE_FUNCTION_NAME = (name: string) =>\n `Duplicate server function \"${name}\" detected. Each server function must have a unique name. Remove or rename the duplicate.`;\n","/** @module Server-side utilities. Exports the `RPCError` class for typed server-side errors, `formatError` for middleware error responses, and `walkGlobFiles` for recursively discovering `*.server.*` files. Never import this module in client code — it is server-only. */\nimport type { JsonObject, JsonValue } from \"./types.d.ts\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { INTERNAL_SERVER_ERROR } from \"./constants.ts\";\n\nconst GLOB_REGEX = /^.+\\.server\\.(ts|js|mjs|mts)$/;\n\n/**\n * Recursively walks `dir` and collects absolute paths to files whose\n * basename matches the `*.server.{ts,js,mjs,mts}` glob pattern.\n */\nexport const walkGlobFiles = async (dir: string): Promise<string[]> => {\n const results: string[] = [];\n const stack = [dir];\n while (stack.length) {\n const current = stack.pop()!;\n let entries;\n try {\n entries = await readdir(current, { withFileTypes: true });\n } catch (_e) {\n continue;\n }\n for (const entry of entries) {\n const fullPath = join(current, entry.name);\n if (entry.isFile() && GLOB_REGEX.test(entry.name)) {\n results.push(fullPath);\n } else if (entry.isDirectory()) {\n stack.push(fullPath);\n }\n }\n }\n return results;\n};\n\n/**\n * A typed error thrown from server functions.\n * The middleware serializes the `message` and `code` in the response,\n * allowing clients to recognise and handle specific error conditions.\n */\nexport class RPCError extends Error {\n /** Machine-readable error code (e.g. \"VALIDATION_FAILED\", \"UNAUTHORIZED\") */\n code: string;\n /** Optional diagnostic payload */\n data?: JsonValue;\n constructor(message: string, code = \"INTERNAL\", data?: JsonValue) {\n super(message);\n this.name = \"RPCError\";\n this.code = code;\n this.data = data;\n }\n}\n\n/**\n * Formats an error for the RCP middleware response.\n * In development the full message and stack are included so developers\n * can quickly identify issues. In production only the generic\n * \"Internal Server Error\" is sent, preventing information disclosure.\n */\nexport const formatError = (\n err: unknown,\n isProduction: boolean,\n): JsonObject => {\n if (!isProduction) {\n if (err instanceof RPCError) {\n const payload: JsonObject = {\n error: err.message || INTERNAL_SERVER_ERROR,\n code: err.code,\n };\n if (err.data !== undefined) payload.data = err.data;\n return payload;\n }\n const msg = err instanceof Error ? err.message : String(err);\n return { error: msg || INTERNAL_SERVER_ERROR };\n }\n return { error: INTERNAL_SERVER_ERROR };\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\";\nimport type { JsonValue } from \"../types.d.ts\";\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\";\nimport { formatError } from \"../server-helpers.ts\";\nimport {\n CLIENT_DISCONNECTED,\n FUNCTION_NOT_FOUND,\n METHOD_NOT_ALLOWED,\n MIDDLEWARE_NAME_USED,\n REQUEST_FORBIDDEN,\n} from \"../constants.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates an Express middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated — reusing a name throws an error.\n * Prefix and path regexes are compiled once at creation time (hoisted) for performance.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns An Express middleware function\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 rpcPrefix = options.rpcPrefix;\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(MIDDLEWARE_NAME_USED(name));\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 = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\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 // rpcPrefix 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/**\n * Creates the Express RPC middleware that routes incoming requests to registered server functions.\n * Reads the request body, dispatches to the matching function via serverFunctionsMap,\n * and sends the JSON-serialized result. Handles client disconnection via abort signals.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns An Express middleware function\n */\nexport const createRPCMiddleware: ExpressMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\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 rpcPrefix = options.rpcPrefix;\n const prefixRegex = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${rpcPrefix}/`;\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: path, searchParams } = 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(path)) {\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 // Optional origin check: reject requests whose Origin header does not\n // match the configured origin. Requests without an Origin header\n // (curl, native clients) pass through unchecked.\n const origin = options.origin;\n const requestOrigin = req.headers.origin;\n if (origin && requestOrigin && requestOrigin !== origin) {\n sendResponse(403, { error: REQUEST_FORBIDDEN });\n return;\n }\n\n const functionName = path.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 method = serverFunction.options?.method || \"POST\";\n if (req.method?.toUpperCase() !== method) {\n sendResponse(405, { error: METHOD_NOT_ALLOWED });\n return;\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = searchParams.get(\"args\");\n // istanbul ignore else\n if (raw) args = JSON.parse(raw);\n } else {\n const body = await readBody(req);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\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 const isProduction = process.env.NODE_ENV === \"production\";\n sendResponse(500, formatError(err, isProduction));\n }\n },\n });\n};\n"],"mappings":"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"}
|