@thednp/rpc 0.0.13 → 0.1.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 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:src # Update all src deps
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 upd # Update all deps (up:examples + up:src + up:root)
24
- pnpm prepublishOnly # upd + lint + check:ts + format + build
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 `prepublishOnly`)
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/Connect middleware are available, which means adapters don't work in DEV mode
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/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
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` — Tips and best practices
144
+ - `wiki/best-practices.md` — Production patterns (auth, rate limiting, body limits, CSRF)
package/CHANGELOG.md ADDED
@@ -0,0 +1,276 @@
1
+ # Changelog
2
+
3
+ ## [0.1.1] - 2026-08-07
4
+
5
+ ### Security
6
+
7
+ - Unexpected exceptions no longer expose their message in development responses: `formatError` returns the generic `{ error: "Internal Server Error" }` for any non-`RPCError` error in every environment. `RPCError` payloads (developer-authored `message`, `code`, optional `data`) are still included in development so client-side error handling keeps working, while diagnostics stay server-side via the middleware's `console.error` logging (addresses the GitHub CodeQL `js/exception-information-leak` finding)
8
+
9
+ ### Refactor
10
+
11
+ - `scanForServerFiles` lazy-imports Vite inside the scan function instead of importing it statically at the top of the module — the standalone server entry no longer carries a static Vite dependency, so serverless function bundles that register API modules directly no longer drag Vite's node chunk (which imports esbuild, absent with Vite 8's rolldown) into the bundle
12
+ - Drop the redundant `config as ScanConfig` casts in `scanForServerFiles` (the merged config is already typed)
13
+
14
+ ### Fixes
15
+
16
+ - Netlify serverless deployment: externalize Vite from the function bundle via `[functions] external_node_modules = ["vite"]` in `netlify.toml`, so `@netlify/zip-it-and-ship-it` no longer fails with "Could not resolve 'esbuild'" when bundling the RPC function
17
+ - Fix broken documentation links in all 6 example READMEs (`wiki/setup.md` → `wiki/wire-protocol.md`, the former never existed)
18
+ - Remove stale `demo/src/render-bak.ts`; clean up the pnpm lockfile
19
+
20
+ ### Docs
21
+
22
+ - Update `wiki/security.md`, `wiki/wire-protocol.md`, `README.md`, and `llms.txt` to reflect that unexpected exception details never reach clients — only `RPCError` payloads, and only in development
23
+
24
+ ### Tests
25
+
26
+ - `formatError` dev-mode tests updated to assert the generic message for unexpected exceptions (plain `Error` and non-`Error` values), keeping the `RPCError` payload assertions
27
+
28
+ ### Chores
29
+
30
+ - Sync demo and all 6 examples to `@thednp/rpc ^0.1.0`
31
+ - Remove the leftover `0.0.14` changelog header (its entry was merged into `0.1.0`)
32
+
33
+ ## [0.1.0] - 2026-08-06
34
+
35
+ ### Features
36
+
37
+ - **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
38
+ - **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
39
+ - **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
40
+ - **`scanRoot` option**: point scanning at any directory (relative to the Vite root), e.g. a shared RPC package in a monorepo
41
+ - **`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
42
+
43
+ ### Refactor
44
+
45
+ - `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)
46
+ - Plugin `options` initialized from `defaultRPCOptions` at creation time, so hooks can be invoked without `configResolved` having run
47
+
48
+ ### Tests
49
+
50
+ - `formatError` unit tests (production vs development, `RPCError` code/data)
51
+ - Multipart `readBody` tests for all four adapters (pre-parsed and raw stream paths)
52
+ - Scan tests: glob mode (recursive + explicit `scanRoot`), duplicate detection (throw in dev, warn in production)
53
+
54
+ ### Chores
55
+
56
+ - 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
57
+ - 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
58
+ - Refactor `getClientModules` to build the generated client modules into a local `entries` const before assembling the output
59
+ - Publish workflow: remove the npm debug-log diagnostic step
60
+ - 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`
61
+
62
+ ## [0.0.13] - 2026-08-04
63
+
64
+ ### Chores
65
+
66
+ - Bump version to `0.0.13`
67
+ - 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
68
+ - 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`
69
+ - Restore the `prepublishOnly` script name in `package.json`
70
+
71
+ ### Docs
72
+
73
+ - Clarify `wiki/client-usage.md` and `wiki/security.md`, `README.md`, and `tsdown.config.ts` comments
74
+
75
+ ## [0.0.12] - 2026-08-03
76
+
77
+ ### Chores
78
+
79
+ - Bump version to `0.0.12`
80
+ - 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)
81
+ - 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`
82
+ - Update all 6 examples to `@thednp/rpc ^0.0.11`
83
+
84
+ ## [0.0.11] - 2026-08-02
85
+
86
+ ### Chores
87
+
88
+ - Bump version to `0.0.11`
89
+ - Publish workflow: use npm only
90
+
91
+ ## [0.0.10] - 2026-08-02
92
+
93
+ ### Chores
94
+
95
+ - Bump version to `0.0.10`
96
+ - 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/`
97
+
98
+ ## [0.0.9] - 2026-08-02
99
+
100
+ ### Chores
101
+
102
+ - Bump version to `0.0.9`
103
+ - Publish workflow: drop `--allow-slow-types` from the jsr publish step (verified clean via `deno publish --dry-run`, no slow-types diagnostics)
104
+
105
+ ## [0.0.8] - 2026-08-02
106
+
107
+ ### Chores
108
+
109
+ - Bump version to `0.0.8`
110
+ - 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
111
+
112
+ ## [0.0.7] - 2026-08-02
113
+
114
+ ### Chores
115
+
116
+ - Bump version to `0.0.7`
117
+ - 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
118
+
119
+ ## [0.0.6] - 2026-08-02
120
+
121
+ ### Docs
122
+
123
+ - 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
124
+ - 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
125
+
126
+ ### Chores
127
+
128
+ - Bump version to `0.0.6`
129
+ - 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
130
+ - 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
131
+
132
+ ## [0.0.5] - 2026-08-02
133
+
134
+ ### TypeScript
135
+
136
+ - 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`
137
+ - 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
138
+
139
+ ### Docs
140
+
141
+ - Update all 6 examples (`spa`, `express`, `fastify`, `hono`, `koa`, `ssr`) to `@thednp/rpc ^0.0.4`
142
+ - 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
143
+
144
+ ### Chores
145
+
146
+ - Bump version to `0.0.5`
147
+ - `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
148
+ - Rebuild `dist/` to pick up the exported types
149
+
150
+ ## [0.0.4] - 2026-08-02
151
+
152
+ ### Breaking
153
+
154
+ - 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)
155
+
156
+ ### Features
157
+
158
+ - Add `method` option to `ServerFunctionOptions` (`"GET" | "POST"`, default `"POST"`) for per-function HTTP method control
159
+ - 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)
160
+ - GET function dispatch: generated client modules send args as a URL-encoded `?args=` JSON query parameter (no request body)
161
+
162
+ ### Security
163
+
164
+ - Enforce method + origin checks in all 4 adapters (Express, Fastify, Hono, Koa) with anchored prefix matching already in place
165
+ - Hono adapter: guard `env.incoming` with optional chaining so bare serverless environments without an incoming stream no longer crash
166
+
167
+ ### Fixes
168
+
169
+ - 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
170
+ - 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`)
171
+
172
+ ### Docs
173
+
174
+ - Add `wiki/best-practices.md` sections on rate limiting and Origin/CSRF protection with framework-specific middleware snippets
175
+ - Document the `method` option in `wiki/server-functions.md`
176
+ - Add Method Enforcement and Origin Validation sections to `wiki/security.md`
177
+ - Document exact scan filename matching in `wiki/setup.md`
178
+ - 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)
179
+ - Add a method-restriction security note (GET/POST only) to the README security section
180
+
181
+ ### Chores
182
+
183
+ - Bump version to `0.0.4`
184
+ - Add test coverage for method dispatch (POST default, GET with `?args=`, 405 enforcement), origin validation (mismatch 403, absent origin passes), and exact scan matching
185
+ - Reach 100% test coverage (232 tests): add `validateMethod` suite, GET client-module fetch test, and GET dispatch coverage for Hono and Koa
186
+ - 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
187
+ - 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`
188
+ - 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`
189
+ - Add `scripts/update-deno.js` to sync JSR metadata (`version`, `description`, `keywords`, `license`) from `package.json` into `deno.json`; add `up:deno` task
190
+ - Refactor `deno.json` tasks to `deno task` self-references; `prepublishOnly` is now `upd` + `lint` + `format` + `audit:src` + `build`
191
+ - Run `pnpm audit` in CI and trigger workflows on `pnpm-workspace.yaml` changes
192
+ - Sync `deno.json` keywords with `package.json` (`vite`, `vite-plugin` added)
193
+
194
+ ## [0.0.3] - 2026-07-30
195
+
196
+ ### Docs
197
+
198
+ - Add comprehensive JSDoc to all exported functions across the codebase (~50 symbols) with `@param` and `@returns` tags
199
+ - Add `@module` JSDoc to all 8 entrypoints for JSR documentation generation
200
+ - Add JSDoc to all exported types, interfaces, and their properties (framework hooks, middleware options, JSON types, adapter types) for JSR documentation scoring
201
+
202
+ ### Chores
203
+
204
+ - Add `description`, `author`, and `keywords` to `deno.json` for JSR metadata
205
+ - Add `imports` map to `deno.json` for JSR self-referencing resolution
206
+ - Bump version to `0.0.3`
207
+
208
+ ## [0.0.2] - 2026-07-30
209
+
210
+ ### Breaking
211
+
212
+ - Rename `rpcPreffix` → `rpcPrefix` across all configs, adapters, types, and docs (the old typo is no longer recognized)
213
+
214
+ ### Features
215
+
216
+ - Add `credentials` option to `ServerFunctionOptions` (`"same-origin" | "include" | "omit"`, default `"same-origin"`)
217
+ - Add `validateCredentials()` to validate the credentials option at build time
218
+ - Rename `rpcPreffix` internal variable consistently to `rpcPrefix`
219
+ - Thread `credentials` through generated client modules and `innerModule`
220
+
221
+ ### Refactor
222
+
223
+ - Centralize all error/warning messages into `src/constants.ts`
224
+ - Extract validation functions (`validateIdentifier`, `validatePathSegment`) into `src/validate.ts` with 100% test coverage
225
+ - Move safe-identifier regex patterns from `constants.ts` to `validate.ts` (implementation detail)
226
+ - Add explicit return types to all adapter helpers and core functions
227
+ - Add `RequestDetails` and `ResponseDetails` types to Express adapter
228
+ - Add `InnerModReturn` helper type to `helpers.ts`
229
+ - Fully type Fastify plugin with explicit interface
230
+ - Type `serverFunctionsMap` explicitly in `functionsMap.ts`
231
+ - Convert `defineConfig` and `loadRPCConfig` to typed arrow functions
232
+
233
+ ### Fixes
234
+
235
+ - Publish workflow: remove `--provenance`, Node version 24
236
+ - Fix AGENTS.md express command, fix Koa adapter docs
237
+ - README GitHub URLs: `rpcv` → `rpc`
238
+ - Proper pnpm workspace setup for StackBlitz
239
+ - StackBlitz compatibility for examples
240
+ - Remove `prepare` script to prevent rolldown native binding error on StackBlitz
241
+ - SPA proxy server: cast `createRPCMiddleware` options for type safety
242
+ - Move adapter deps from `devDependencies` to `dependencies` for correct runtime resolution
243
+
244
+ ### Docs
245
+
246
+ - Expand Authentication section in best-practices with Basic Authorization + Per-Function Authorization examples
247
+ - Add CONTRIBUTING section to README
248
+ - Update all wiki docs to use `rpcPrefix`
249
+ - Clarify isomorphic nature of server functions in wiki
250
+ - Add README.md to each example
251
+ - Use full wiki URLs in example READMEs
252
+ - Fix broken link in security.md (`best-practives.md` → `best-practices.md`)
253
+ - Fix cancellation description in client-usage.md to match actual behavior
254
+
255
+ ### Chores
256
+
257
+ - Create CHANGELOG.md
258
+ - Update `dist/` with latest builds
259
+ - Update examples dependencies
260
+ - Bump version to `0.0.2`
261
+ - Add `keywords` field to package.json
262
+ - Move `picocolors` from root to examples/spa
263
+
264
+ ## [0.0.1] - 2026-07-28
265
+
266
+ ### Initial Release
267
+
268
+ - Vite plugin for automatic RPC generation
269
+ - Framework-agnostic core with adapters for Express, Fastify, Hono, and Koa
270
+ - Auto-scanning of server functions via `scanForServerFiles`
271
+ - Client-side module generation at build time
272
+ - Request cancellation via `AbortController`
273
+ - Prefix-gated RPC endpoint with regex boundary protection
274
+ - Code injection prevention in client module generation
275
+ - SSR and SPA support
276
+ - 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
- Most RPC solutions ask you to adopt a new way of thinking, require learning a complex API, some are vendor locked, some even allow you to blend in with your client code (via `"use server"` directive), for sure they are powerful and work well, they provide excellent DX, but complexity always comes with its own drawbacks.
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` carves out the niche that wants to do RPC **without the weight of an entire framework**. If your app is a Vite site, a static SPA, or a small server powered by a single middleware — but you still want typed, cancellable, server-only functions callable from the client — you shouldn't have to adopt a full meta-framework, a full-stack router, or a build-time convention just to bridge the two. This plugin gives you that bridge alone: no framework to learn, no runtime to adopt, no vendor to sign up with.
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 stub generation, middleware registration, request cancellation — without asking you to restructure your codebase.
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` — no messages, codes, or stacks leak to clients, in any environment. Only `RPCError` payloads (developer-authored `message`/`code`/`data`) reach the client, and only in development, 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. There are 6 test files covering all adapters plus the plugin.
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
- - [Getting Started](./wiki/getting-started.md)
311
- - [Setup Guide](./wiki/setup.md)
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;;;;;;;;;;;cCtCW,kBAAkB;;;;;;;;cAyElB,qBAAqB;;;;;;;;iBC5FZ,UAAU,KAAK,UAAO;;;;;;iBAW5B,WAAW,KAAK,SAAS,MAAM;;;;;;;;cAWlC,WAAQ,KACd,UAAiB,oBACrB,QAAQ;;;;;;cA2DE,mBAAgB,KACtB,kBAAkB,YACtB,OAAO;;;;;;cASG,oBAAiB,KACvB,iBAAiB,aACrB,OAAO;;;;;;;cAUG,mBAAgB,KACtB,kBAAkB,YACtB,OAAO;;;;;;;cAUG,oBAAiB,SACnB,UAAiB,oBACzB;;;;;;;cAqBU,qBAAkB,UACnB,WAAkB,mBAC3B"}
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;;;;;;iBAc5B,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"}
@@ -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 isJSON = (req.headers["content-type"]?.toLowerCase() || "").includes("json");
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 isJSON = (req.headers["content-type"]?.toLowerCase() || "").includes("json");
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 RPC middleware response.
202
+ * In development the full `RPCError` payload is included so developers
203
+ * can quickly identify issues. Unexpected exceptions never expose their
204
+ * message — only the generic "Internal Server Error" is sent, preventing
205
+ * information disclosure; server-side diagnostics are preserved via the
206
+ * middleware's `console.error` logging.
207
+ */
208
+ const formatError = (err, isProduction) => {
209
+ if (isProduction) return { error: INTERNAL_SERVER_ERROR };
210
+ if (err instanceof RPCError) {
211
+ const payload = {
212
+ error: err.message || "Internal Server Error",
213
+ code: err.code
214
+ };
215
+ if (err.data !== void 0) payload.data = err.data;
216
+ return payload;
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
- sendResponse(500, { error: INTERNAL_SERVER_ERROR });
309
+ const isProduction = process.env.NODE_ENV === "production";
310
+ sendResponse(500, formatError(err, isProduction));
265
311
  }
266
312
  }
267
313
  });