@thednp/rpc 0.3.5 → 0.3.7

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.
Files changed (45) hide show
  1. package/AGENTS.md +39 -12
  2. package/CHANGELOG.md +165 -0
  3. package/README.md +2 -2
  4. package/dist/config/config.d.mts +0 -7
  5. package/dist/config/config.d.mts.map +1 -1
  6. package/dist/config/config.mjs +5 -1
  7. package/dist/config/config.mjs.map +1 -1
  8. package/dist/express/express.d.mts +62 -32
  9. package/dist/express/express.d.mts.map +1 -1
  10. package/dist/express/express.mjs +71 -23
  11. package/dist/express/express.mjs.map +1 -1
  12. package/dist/fastify/fastify.d.mts +81 -47
  13. package/dist/fastify/fastify.d.mts.map +1 -1
  14. package/dist/fastify/fastify.mjs +73 -23
  15. package/dist/fastify/fastify.mjs.map +1 -1
  16. package/dist/fastify/plugin/fastify/plugin.d.mts +30 -55
  17. package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -1
  18. package/dist/fastify/plugin/fastify/plugin.mjs +72 -22
  19. package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
  20. package/dist/h3/h3.d.mts +73 -4
  21. package/dist/h3/h3.d.mts.map +1 -1
  22. package/dist/h3/h3.mjs +69 -22
  23. package/dist/h3/h3.mjs.map +1 -1
  24. package/dist/helpers/helpers.d.mts.map +1 -1
  25. package/dist/helpers/helpers.mjs +2 -0
  26. package/dist/helpers/helpers.mjs.map +1 -1
  27. package/dist/hono/hono.d.mts +71 -6
  28. package/dist/hono/hono.d.mts.map +1 -1
  29. package/dist/hono/hono.mjs +94 -24
  30. package/dist/hono/hono.mjs.map +1 -1
  31. package/dist/index.d.mts +43 -25
  32. package/dist/index.d.mts.map +1 -1
  33. package/dist/index.mjs +62 -28
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/koa/koa.d.mts +73 -11
  36. package/dist/koa/koa.d.mts.map +1 -1
  37. package/dist/koa/koa.mjs +75 -23
  38. package/dist/koa/koa.mjs.map +1 -1
  39. package/dist/server/server.d.mts +159 -18
  40. package/dist/server/server.d.mts.map +1 -1
  41. package/dist/server/server.mjs +186 -15
  42. package/dist/server/server.mjs.map +1 -1
  43. package/llms.txt +5 -3
  44. package/package.json +5 -3
  45. package/CLAUDE.md +0 -1
package/AGENTS.md CHANGED
@@ -13,7 +13,7 @@ pnpm dev:koa # Run examples/koa dev server
13
13
  pnpm dev:react-query # Run examples/react-query dev server
14
14
  pnpm dev:solid-query # Run examples/solid-query dev server
15
15
  pnpm dev:ssr # Run examples/ssr dev server
16
- pnpm lint # Lint + typecheck (deno lint + tsc)
16
+ pnpm lint # Lint + typecheck (deno lint + tsc src + tsc tests)
17
17
  pnpm test # Run tests once with coverage (vitest run --coverage)
18
18
  pnpm test:watch # Run tests in watch mode with coverage
19
19
  pnpm test:ui # Run tests with UI
@@ -23,6 +23,7 @@ pnpm lint:ts # deno lint src
23
23
  pnpm fix:ts # deno lint src --fix
24
24
  pnpm check:ts # tsc -noEmit
25
25
  pnpm format # deno fmt src tests examples/**/src
26
+ pnpm format:check # deno fmt --check (what CI runs — never rewrites)
26
27
  pnpm clean # Remove build artifacts and caches
27
28
  pnpm build # tsdown (outputs to dist/)
28
29
  pnpm up:examples # Update all example deps (to latest published @thednp/rpc + latest example deps)
@@ -95,22 +96,38 @@ The tsdown.config.ts produces multiple entries:
95
96
  | ---------------------------------| --------------------------------------------------------------------| -----|
96
97
  | `tests/plugin.test.ts` | Plugin init, loadRPCConfig, createServerFunction, getClientModules | |
97
98
  | `tests/scan.test.ts` | scanForServerFiles (real scan, skip, devServer, error handling) | |
98
- | `tests/server-helpers.test.ts` | RPCError, formatError, redirect, glob walking | |
99
- | `tests/client-helpers.test.ts` | Client fetch stubs and retrieval helpers | |
99
+ | `tests/server-helpers.test.ts` | RPCError, formatError, redirect, glob walking, origin allowlist (`isOriginAllowed`) and the four-tier `isOriginRequestAllowed` behaviour matrix | |
100
+ | `tests/client-helpers.test.ts` | Client fetch stubs, retrieval helpers, `unwrapEnvelope` error contract | |
100
101
  | `tests/context.test.ts` | provideRequestContext / getRequestContext (AsyncLocalStorage) | |
102
+ | `tests/adapter-exports.test.ts` | Export-surface contract: asserts each adapter's emitted `.d.mts` exports the required type names (build-output test — run `pnpm build` first) | |
101
103
  | `tests/express.test.ts` | Express helpers, createMiddleware, createRPCMiddleware | |
102
104
  | `tests/fastify.test.ts` | Fastify helpers, plugin, createMiddleware, createRPCMiddleware | |
103
105
  | `tests/h3.test.ts` | h3 helpers, viteMiddleware, createMiddleware, createRPCMiddleware | |
104
106
  | `tests/hono.test.ts` | Hono helpers, createMiddleware, createRPCMiddleware | |
105
107
  | `tests/koa.test.ts` | Koa helpers, createMiddleware, createRPCMiddleware | |
106
108
 
109
+ Run `pnpm build` before `pnpm test` — `tests/adapter-exports.test.ts` reads the emitted declarations and hard-fails without them, and the adapters themselves import `@thednp/rpc/server` (aliased to `src/server.ts` under vitest, so only that one suite needs the build). CI now builds first: `dist/` is **committed**, so without that step the suite silently validated the last-committed bundle and a type export removed or renamed in `src/` went unnoticed (demonstrated: deleting `ExpressApp` from `src/express/types.d.ts` passed 56/56 with no build, fails with one). `tsc` has no `paths` mapping for `@thednp/rpc/server`, so it type-checks against `dist/` — a source change that adds an export needs a rebuild before `pnpm lint` will accept it.
110
+
107
111
  ## Important Notes
108
112
 
109
113
  - 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
110
114
  - Uses `deno` for linting and formatting (not eslint/prettier)
111
115
  - Uses `tsdown` for bundling (not rollup/vite directly)
112
116
  - Uses `vitest` for testing with `istanbul` coverage
113
- - The `iscScanned` module-level flag in `scanForServerFiles.ts` persists across tests — use `devServer` parameter or reset modules to bypass
117
+ - The `scannedTargets` memo in `scanForServerFiles.ts` persists across tests — reset modules to bypass, or vary the `(scanRoot, serverFiles, rpcPrefix)` triple. It is keyed per target rather than being one process-wide boolean, which is what lets a second prefix be scanned after the first; do not collapse it back to a single flag
118
+ - **Coverage measures line execution, not feature correctness.** Two bugs shipped at 100% coverage because every fixture in `tests/fixtures/*.ts` calls `setGlobalPrefix(undefined)`, so the only state the global-prefix path cares about was never exercised. When adding a test, assert *which* code path ran — not just that output is non-empty (the vite 7/8 transform tests did the latter, which is why the `Number(viteVersion[0])` bug hid)
119
+ - `setGlobalPrefix` is stored on `globalThis[Symbol.for("thednp.rpc.globalPrefix")]`, so probes can read it without importing the module
120
+
121
+ ## Fixed in 0.3.7 — prefix resolution
122
+
123
+ A 2026-09-27 audit found the global prefix and the dispatch prefix were resolved by two independent pieces of logic that could disagree. Both are now fixed; the notes below are the guardrails.
124
+
125
+ - **`resolveRPCPrefix(rpcPrefix?)` (`src/server-helpers.ts`) is the single resolution point** — explicit argument → `getGlobalPrefix()` → `defaultPrefix`. All five adapters call it in *both* places they need it (the outer `createMiddleware` gate and the `createRPCMiddleware` dispatch), so parity is structural rather than a convention. Do not reintroduce a local `a || b || c` in an adapter
126
+ - **`createRPCMiddleware` no longer injects `{ rpcPrefix: defaultRPCOptions.rpcPrefix }`.** That default is what made the `|| getGlobalPrefix()` fallback unreachable: `rpcPrefix` was always the truthy `"__rpc"`, so with `setGlobalPrefix("@demo")` a function registered under `@demo` (via `createServerFunction`, which does honour the global prefix) was unreachable — `/@demo/greet` got `next()` and `/__rpc/greet` got `"Function not found"`. The resolved prefix is now passed down from `createRPCMiddleware` to the gate as an explicit value
127
+ - **The boundary regex is built from the resolved prefix, and only when a prefix is supplied.** Two traps here: building it unconditionally would start prefix-gating a bare `createMiddleware({ path, handler })`, which has never gated; building it from the *raw* argument would make it `null` and silently disable gating. The gate gates on `rpcPrefix ? ... : null`; the value is the resolved prefix
128
+ - **`loadRPCConfig` publishes the global prefix on every return path**, including the default config-file discovery loop — that is the common case, and it used to be the one path that skipped the call
129
+ - **Known, deliberately unchanged:** a config file that throws resets the module-level `RPCConfig` cache to the defaults. The "fall back to defaults" contract is asserted by tests, so a failed load downgrades a previously loaded config for the rest of the process. `MiddlewareOptions.rpcPrefix` also declared a `false` that was documented nowhere, tested nowhere, and handled nowhere — measured as byte-identical to omitting the option. Removed from the type in the same release; do not reintroduce it without implementing and documenting what it does, since a `false` that reads like "disable prefix gating" while gating on the default is the more dangerous shape
130
+ - **The coverage lesson stands:** both bugs shipped at 100% coverage because every fixture in `tests/fixtures/*.ts` called `setGlobalPrefix(undefined)`, so the state the feature is *about* was never exercised. `tests/express.test.ts` now has a `global-prefix dispatch` block that sets a real prefix, and `tests/plugin.test.ts` has a file-level `afterEach` resetting it — do not remove either
114
131
 
115
132
  ## Framework
116
133
 
@@ -119,24 +136,28 @@ The tsdown.config.ts produces multiple entries:
119
136
  - Framework-agnostic core with adapters for Express, Fastify, Hono, Koa, and h3
120
137
  - Client modules are auto-generated with `AbortController` support for cancellation
121
138
  - Server-side caching must be handled by third party tools (e.g. `@tanstack/react-query`)
122
- - **Multi-prefix support**: `createServerFunction(..., { rpcPrefix })` registers functions in a prefix-scoped map (`getFunctionsForPrefix`), so multiple RPC instances can coexist (versioned/namespaced APIs). All five adapters dispatch via `getFunctionsForPrefix(rpcPrefix || defaultPrefix)`; `serverFunctionsMap` is a backward-compatible proxy for the default `"__rpc"` prefix (`defaultPrefix`)
139
+ - **Multi-prefix support**: `createServerFunction(..., { rpcPrefix })` registers functions in a prefix-scoped map (`getFunctionsForPrefix`), so multiple RPC instances can coexist (versioned/namespaced APIs). All five adapters dispatch via `getFunctionsForPrefix(prefix)` where `prefix = rpcPrefix || getGlobalPrefix() || defaultPrefix`; `serverFunctionsMap` is a backward-compatible proxy for the default `"__rpc"` prefix (`defaultPrefix`)
123
140
 
124
141
  ## Security & Hardening
125
142
 
126
143
  - **Prefix boundary check**: All adapters use `new RegExp(\`^/${escapeRegExp(rpcPrefix)}/\`)` instead of `startsWith` to prevent path segment bypassing (e.g., `/__rpc-evil/foo` no longer matches prefix `"__rpc"`)
127
144
  - **Prefix regex injection prevention**: `rpcPrefix` config string is escaped via `escapeRegExp()` before being embedded in the boundary regex, preventing ReDoS or unintended matching from metacharacters in the prefix
128
145
  - **Regex compilation hoisted**: All prefix/path regexes are compiled once at middleware creation time (not per-request), eliminating per-request regex overhead
129
- - **Koa URL normalization** → **URL normalization (all adapters)**: every adapter parses the request URL through the shared `safeURL()` helper (`src/server-helpers.ts`) to strip query strings and normalize encoding before prefix checking. `safeURL` **never throws** — malformed request-targets (`/\`, `//`, `/\/`) make the WHATWG parser raise `TypeError: Invalid URL`, and the adapters parse the URL *before* their dispatch `try` block, so an unguarded throw became an unhandled rejection that crashed raw `node:http` hosts and Express 4. On failure it falls back to the base root, so the pathname never matches the prefix and the request degrades to `next()`/404
146
+ - **Koa URL normalization** → **URL normalization (all adapters)**: every adapter normalizes the request URL before prefix checking. `safeURL()` (`src/server-helpers.ts`) **never throws** — malformed request-targets (`/\`, `//`, `/\/`) make the WHATWG parser raise `TypeError: Invalid URL`, and the adapters parse the URL *before* their dispatch `try` block, so an unguarded throw became an unhandled rejection that crashed raw `node:http` hosts and Express 4. On failure it falls back to the base root, so the pathname never matches the prefix and the request degrades to `next()`/404. The call site differs per adapter, so "all adapters use `safeURL`" is only *nearly* true: **fastify/hono/koa** call it directly, **express** reaches it through `getRequestDetails(req)` in `src/express/helpers.ts`, and **h3 does not call it at all** — it reads `event.url.pathname`, which h3 has already parsed, so the throw-on-malformed-target case is handled upstream by h3 rather than by `safeURL`
130
147
  - **GET `?args=` array validation**: all five adapters `JSON.parse` the query value and reject anything that is not an array with `400 Bad Request` before dispatch. Without the guard, `?args={"a":1}` spread an object into `handler(...args)` (`TypeError`) and `?args="abc"` spread a string into characters — confusing 500s on attacker-controlled input
131
148
  - **Code injection prevention in client module generation**: `getClientModules.ts` validates all interpolated identifiers (`fnName`, `fnEntry`, `rpcPrefix`) against `/^[A-Za-z_$][A-Za-z0-9_$]*$/` (and a path-safe variant allowing `/`, `@`, `:`, `-`) before interpolating into the generated client bundle. This prevents code injection via malicious export names or prefixes containing template literal interpolations (`${...}`), backticks, or `</script>` sequences.
132
149
  - **Body size limits**: Host frameworks cap parsed JSON bodies — Express (`express.json({ limit })`), Fastify (`bodyLimit`), Koa (`koa-body`), Hono (`hono/body-limit`), h3 (`bodyLimit`/`assertBodySize`). Rely on your framework's body parser middleware for size limits (see wiki/best-practices.md). The raw stream path in `readBody` does not impose a built-in limit — use framework middleware or a custom body limit handler for defense-in-depth. **Enforce any custom cap while streaming, never after buffering**: `examples/spa/body-limit.ts` measures each chunk as it arrives, retains nothing past the cap, then drains-and-discards so the `413` is deliverable (closing a socket with unread request data makes Node emit `RST`) with a drain ceiling so the discard can't become an unbounded slowloris. Buffering first and measuring after — the obvious `readBody`-then-check shape — provides no memory-exhaustion protection at all
150
+ - **A malformed request is a `4xx`, never a `500`, and never a silent `200`**: a declared-JSON body that does not parse answers `400`, as does a GET `?args=` that is malformed or not an array. Three helpers in `src/server-helpers.ts` implement this once for all five adapters — `httpError(status, message)` tags an error for the dispatch, `isClientHttpError(err)` decides the class, and `clientErrorMessage(status)` picks the body from a fixed table. Read **both** `status` and `statusCode`: h3's `HTTPError` and `httpError` use the former, the `http-errors` objects Express's `body-parser` throws and Koa's `ctx.throw` use the latter. `readBody` must only `JSON.parse` when the content type actually declared JSON — the lenient sniff for undeclared bodies is deliberate (curl and the nojs fallback send JSON with no `Content-Type`) and an earlier version had all three branches fall through to one `JSON.parse`, so a text body threw and was "recovered" as `text/plain`, which made malformed JSON indistinguishable from a text body and answered `200`
151
+ - **Hono's `c.env` is optional**: only `@hono/node-server` populates it. Workers, Bun, Deno, serverless adapters and `app.fetch()` all leave it `undefined`, so every `c.env` read needs `?.` — `c.env.incoming?.` is not enough, since that guards a null `incoming`, not an absent `c.env`. `tests/hono.test.ts` drives a real `Hono` app through `app.fetch()` for this reason
133
152
  - **Content-type strictness is one-directional**: JSON- and text-declared functions are strict (a form body is rejected with `415`); form-declared functions accept either form encoding, which is what makes the nojs `<form>` fallback work. The leniency does not run the other way
134
153
  - **Generic 404 responses**: Error messages never echo the requested function name (no message-based function enumeration). Note the status code still distinguishes unknown (`404`/`400`) from known functions (`405`/`415`/`403`); function names ship in the client bundle so they are not secret — see `wiki/security.md`
135
154
  - **Client error contract is shared, not fail-open**: `handleResponse` (used by generated stubs) and `unwrapEnvelope` (exported for native clients) agree — a top-level `error` key throws, `{ data: { error } }` resolves normally so validation-as-data keeps working. `unwrapEnvelope` is status-code agnostic, so callers must keep the `res.ok` check. `RPCError` is server-side only (`@thednp/rpc/server`); it is not a client export and its `code`/`data` are stripped in production regardless
136
- - **Origin allowlist is shared, not copy-pasted**: the `origin` option accepts a single string **or an array** (`string | string[]`). The rule lives once in `isOriginAllowed(allowed, requestOrigin)` (`src/server-helpers.ts`), used by all five adapters — a request is `403` only when it carries an `Origin` header matching none of the entries. Headerless requests pass through; unset means no validation. Matching is exact, so `https://app.example.com.evil.com` and `https://app.example.com:443` are rejected. `Origin: null` (sandboxed iframes, `file://`, extensions) is rejected whenever an allowlist is set. A string and a one-element array are equivalent, so widening was backward compatible
155
+ - **Origin allowlist is shared, not copy-pasted**: the `origin` option accepts a single string **or an array** (`string | string[]`). The rule lives once in `isOriginRequestAllowed(allowed, origin, site)` (`src/server-helpers.ts`), used by all five adapters. Four tiers, first signal wins: `origin` unset → pass; `Origin` present → the allowlist decides (exact match, so `https://app.example.com.evil.com` and `https://app.example.com:443` are rejected, and `Origin: null` never matches); `Origin` absent but `Sec-Fetch-Site` present → allow only `same-origin`/`none`; both absent → pass (the deliberate curl/native hole). `Origin` **must** short-circuit ahead of `Sec-Fetch-Site` — the allowlist exists to admit a sibling subdomain, whose request carries `Sec-Fetch-Site: same-site`, which tier 3 alone would reject. A regression test pins the ordering. Empty/whitespace header values count as absent, because adapters disagree on what a missing header yields (Node `undefined`, Hono `c.req.header()` `""`). `isOriginAllowed` is public API and unchanged — it is now the tier-2 building block. A string and a one-element array are equivalent
156
+ - **Adapter type exports are uniform**: every adapter re-exports `<Fw>App` / `<Fw>Request` / `<Fw>Response` / `<Fw>Next` / `<Fw>MiddlewareFn` / `<Fw>MiddlewareOptions` / `<Fw>MiddlewareHooks`, plus the shared `RequestDetails` / `ResponseDetails` (defined once in `src/adapter-types.ts`, re-exported by all five), so a wrapper can annotate without depending on the framework. Additive only — legacy names (`Express`, `Fastify`, `Hono`, `Koa`, `H3Event`, `HonoContext`, `KoaContext`) still resolve. `tests/adapter-exports.test.ts` parses the emitted `dist/<adapter>/<adapter>.d.mts` to guard this, because type-only exports are erased from the `.mjs` and invisible to a runtime check
137
157
  - **Auth is middleware's responsibility**: Authentication should be handled by middleware registered before `createRPCMiddleware()`. The middleware chain naturally composes — no built-in auth hook is needed.
138
158
  - **No client-side secrets or stack traces**: Error responses always return `"Internal Server Error"` regardless of the underlying error; `console.error(String(err))` is server-side only for debugging and does not surface internals to the client
139
- - **Prefix parity across adapters**: all five resolve the dispatch prefix as `rpcPrefix || getGlobalPrefix() || defaultPrefix`. A mismatch here is fail-closed (404, never a cross-prefix dispatch) but still a bug — keep the five in sync
159
+ - **There is no `adapter` config option, by design.** The adapter is the subpath you import. A runtime value could only disagree with the subpath actually mounted, and nothing read it — it was inert for its whole life. The union survives only as the exported `AdapterName` type, which keys `FrameworkHooks[A]["handler"]`; each adapter hardcodes its own literal into `MiddlewareOptions<"…">`. Do not reintroduce a config field that selects it. The generated client stubs are plain `fetch` calls, so `getClientModules` needs only the prefix
160
+ - **Prefix parity across adapters**: enforced by `resolveRPCPrefix()`, not by convention — see *Fixed in 0.3.7*. A mismatch would be fail-closed (404, never a cross-prefix dispatch) but still a bug
140
161
 
141
162
  ## Threat Model
142
163
 
@@ -146,7 +167,8 @@ The framework's security boundary is the **RPC prefix-gated HTTP endpoint**. Inp
146
167
  | -----------------------| -------------------------------| ---------------------| --------------------------------------------------------------------------|
147
168
  | `rpcPrefix` (config) | `rpc.config.ts` / dev options | Developer-trusted | Escaped before regex; validated before code gen |
148
169
  | Function export name | `src/api/server.ts` exports | Developer-trusted | Validated against identifier regex before client codegen |
149
- | HTTP request URL | Untrusted client | Boundary-filtered | Prefix regex (escaped, anchored, hoisted); non-throwing `safeURL()` normalization |
170
+ | HTTP request URL | Untrusted client | Boundary-filtered | Prefix regex (escaped, anchored, hoisted); non-throwing URL normalization (`safeURL()` on fastify/hono/koa, via `getRequestDetails` on express, pre-parsed `event.url` on h3) |
171
+ | HTTP request headers (`Origin`, `Sec-Fetch-Site`) | Untrusted client | Boundary-filtered | Exact-match allowlist; `Origin` short-circuits ahead of `Sec-Fetch-Site`; fails closed when only the coarse signal survives; empty values treated as absent |
150
172
  | HTTP request body | Untrusted client | Capped by framework | Framework body parsers cap JSON and raw bodies |
151
173
 
152
174
  **Attackers cannot**:
@@ -154,6 +176,8 @@ The framework's security boundary is the **RPC prefix-gated HTTP endpoint**. Inp
154
176
  - Bypass the prefix via segment-prefixing tricks (anchored regex, not `startsWith`)
155
177
  - Trigger ReDoS via prefix metacharacters (escaped before compilation)
156
178
  - Exhaust memory via large raw text bodies (framework body parsers enforce limits)
179
+ - Sneak past the origin allowlist with a lookalike host (`https://app.example.com.evil.com` is rejected — matching is exact, never a prefix test)
180
+ - Downgrade the origin check by stripping `Origin` and leaving `Sec-Fetch-Site: cross-site` (that combination is `403`; only `same-origin`/`none` pass once the precise signal is gone)
157
181
 
158
182
  **Attackers are expected to**:
159
183
  - Be free to send as many requests as the host allows (no rate limiting — host's responsibility)
@@ -172,10 +196,13 @@ The framework's security boundary is the **RPC prefix-gated HTTP endpoint**. Inp
172
196
  - `wiki/getting-started.md` — Installation, project structure, auto-scanning, and your first function
173
197
  - `wiki/configuration.md` — Configuration reference (`rpc.config.ts`, `vite.config.ts`, options)
174
198
  - `wiki/server-functions.md` — `createServerFunction` API, methods, validation, **request context (`getRequestContext`/`provideRequestContext`)** for per-request data access across async call stacks
175
- - `wiki/middleware.md` — universal adapter-agnostic middleware via the request context (`locals` bridge, `getRequestMeta`, `sendResponse`, `functionName`)
199
+ - `wiki/multi-prefix-guide.md` — Parallel RPC instances: versioned/public/admin API layouts, per-prefix middleware, canary deployments, origin validation per instance
200
+ - `wiki/middleware.md` — universal adapter-agnostic middleware via the request context (`locals` bridge, `getRequestMeta`, `sendResponse`, `functionName`), plus **handler wrappers** — the portable way to populate `event.locals` that works on all five adapters (including Fastify, which has no per-request store to bridge). Prefer the wrapper pattern over per-adapter reads when documenting middleware
176
201
  - `wiki/nojs-fallback.md` — native (no-JS) `<form>` fallback / progressive enhancement pattern
177
- - `wiki/client-usage.md` — Client-side usage, type safety, react-query integration
202
+ - `wiki/client-usage.md` — Client-side usage, type safety, react-query integration, native clients via `unwrapEnvelope`
178
203
  - `wiki/wire-protocol.md` — HTTP contract, request/response bodies, curl debugging
179
- - `wiki/adapters.md` — Framework adapters (Express, Fastify, Hono, Koa, h3)
204
+ - `wiki/adapters.md` — Framework adapters (Express, Fastify, Hono, Koa, h3) and the re-exported framework type contract
180
205
  - `wiki/security.md` — Security hardening
206
+ - `wiki/comparison.md` — How the cross-origin/CSRF boundary compares to Next.js Server Actions, TanStack Start, SvelteKit, tRPC, and (in a section) Vike/Telefunc. **Read this before writing any security copy.** The framing is *fail-open by default, strictest-once-configured, multi-origin without a proxy* — measured, not asserted: with `origin` set, rpc rejects an untrusted `Origin` even when `Sec-Fetch-Site: same-origin` claims otherwise, which TanStack's tier order waves through. Do **not** soften this into "as secure as" or "more secure than" — the page's own numbers (3 stricter, 2 more lenient, both deliberate) are the honest shape, and a reader who knows the tools will check. The `Where the trade costs you` section keeps rpc's real sharp edges (case-sensitive origin matching, literal origins only, no automatic input validation, deliberate `Referer` omission)
181
207
  - `wiki/best-practices.md` — Production patterns (auth, rate limiting, body limits, CSRF)
208
+ - `wiki/index.md` — Documentation index / table of contents
package/CHANGELOG.md CHANGED
@@ -1,5 +1,170 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.7] - 2026-09-27
4
+
5
+ A correctness pass over the transport, found by auditing the codebase against its
6
+ own documentation. Two of the three headline bugs were found *by* that audit
7
+ being unable to corroborate the docs — they were documented, tested-adjacent, and
8
+ wrong.
9
+
10
+ **What was broken**
11
+
12
+ - **Hono's RPC returned `500` for every request on any runtime that is not
13
+ `@hono/node-server`** — Workers, Bun, Deno, standalone serverless, and Hono's
14
+ own `app.fetch()`. Three unguarded `c.env` reads, and a disconnect hook whose
15
+ comment said "may be absent ... so guard the close hook" followed by
16
+ `c.env.incoming?.`, which guards a null `incoming` rather than an absent
17
+ `c.env`. The fixtures always set `env`, so nothing caught it.
18
+ - **A malformed declared-JSON body was answered `200` with the raw string handed
19
+ to the handler** on Express, Koa and Fastify, and `500` on Hono and h3. The
20
+ cause was a `readBody` ternary with no `isJSON` branch: text bodies also went
21
+ through `JSON.parse`, so a malformed JSON body and a legitimate text body threw
22
+ the same exception and were indistinguishable. The `200` failed *open*.
23
+ - **The documented global-prefix and serverless flows returned `404` on every
24
+ adapter**, and `loadRPCConfig` skipped publishing the prefix on its most
25
+ common code path.
26
+ - **Only the first scan in a process ran**, so a second RPC instance on its own
27
+ prefix could never populate its map.
28
+ - **A CI gap let the export-surface test pass while the type it guards was
29
+ deleted** — it is a build-output test, and the build step was commented out.
30
+
31
+ **What changed underneath**: a malformed request is now a `4xx` and never a
32
+ silent `200` or a `500`, via one shared rule used by all five adapters; the
33
+ prefix resolves through a single `resolveRPCPrefix()`; and two config options
34
+ that never did anything (`adapter`, and `rpcPrefix: false`) are gone, with the
35
+ one type that carries real meaning extracted as `AdapterName`.
36
+
37
+ This release **does** change the emitted bundles — `src/index.ts`,
38
+ `src/scanForServerFiles.ts`, `src/server-helpers.ts`, `src/getClientModules.ts`,
39
+ `src/constants.ts`, `src/options.ts`, `src/types.d.ts` and all five
40
+ `src/*/createMiddleware.ts` + `src/*/helpers.ts` files — so `dist/` is rebuilt
41
+ and must be committed with the source. **Breaking**: express/koa/fastify now
42
+ answer `400` where they answered `200` for a malformed body, and a config
43
+ carrying `adapter:` now fails typecheck rather than being ignored.
44
+
45
+ ### ⚠️ Behaviour change — prefix resolution
46
+
47
+ - **The documented global-prefix and serverless flows returned 404 on all five adapters.** `createRPCMiddleware` merged `{ rpcPrefix: defaultRPCOptions.rpcPrefix }` into its options *before* resolving the prefix, so `rpcPrefix` was always the truthy `"__rpc"` and the trailing `rpcPrefix || getGlobalPrefix() || defaultPrefix` was unreachable. `createServerFunction` *does* honour the global prefix, so the two halves disagreed — with `setGlobalPrefix("@demo")`, a middleware built without an explicit prefix answered `next()` for `/@demo/greet` (falling through to the app) and `"Function not found"` for `/__rpc/greet`. `attachRPC` escaped this only because it threads the loaded config through explicitly, which is why every example passed.
48
+ The default injection is gone, and all five adapters now resolve through a new `resolveRPCPrefix()` (`src/server-helpers.ts`) used in *both* the outer `createMiddleware` gate and the `createRPCMiddleware` dispatch, so parity is structural rather than a convention. **The change only affects states that were already returning 404:** with no global prefix set — every example and every other test — the resolved prefix is still `"__rpc"`, and an explicitly-passed prefix still wins. The boundary regex is built from the *resolved* prefix so gating is never dropped.
49
+ - **`loadRPCConfig` now publishes the global prefix on every return path.** It returned from inside its config-file search loop without calling `setGlobalPrefix`, while the explicit-`configFile` and no-config paths both did — so the common case (an `rpc.config.ts` exists) was the one that skipped it. Masked in the plugin path because `scanForServerFiles` re-resolves the prefix itself; exposed in the `attachRPC` / direct-import path.
50
+
51
+ Both bugs shipped at 100% coverage because every fixture in `tests/fixtures/*.ts` called `setGlobalPrefix(undefined)`, so the state the feature is *about* was never exercised. `tests/express.test.ts` now has a `global-prefix dispatch` block (6 tests) that sets a real prefix, and `tests/plugin.test.ts` has a file-level `afterEach` resetting it.
52
+
53
+ One related thing was investigated and deliberately left alone: a config file that throws still resets the `RPCConfig` cache to the defaults, because the "fall back to defaults" contract is asserted by tests. (The `rpcPrefix: false` phantom found by the same audit is handled below.)
54
+
55
+ ### ⚠️ Breaking — the inert `adapter` config option is gone
56
+
57
+ - `RpcPluginOptions.adapter` (`'express' | 'hono' | 'h3' | 'fastify' | 'koa'`) was **never read by anything**. Exactly two places in the whole source touched its value: `src/index.ts` destructured it out and discarded it (`const { adapter: _adapter, ...rest } = options`), and passed it into `getClientModules`, which spread it into a helper's options and never looked at it. The adapter is determined entirely by which subpath you import — `@thednp/rpc/express`, `/hono`, `/koa`, `/h3`, `/fastify` — which is exactly how the type system already modelled it, each adapter hardcoding its own literal into `MiddlewareOptions<"express">` etc.
58
+ A runtime value could therefore only ever *disagree* with the subpath actually mounted, and nothing read it to notice. Setting `adapter: "hono"` while mounting Express silently did nothing. The field is removed; the **union survives as the exported `AdapterName` type**, which is what keys `FrameworkHooks[A]["handler"]` at `src/types.d.ts:383` and is genuinely load-bearing.
59
+ Removed alongside it: `adapter` from `RpcPluginOptionsInternal` (which is why `getClientModules` only ever needed `rpcPrefix`), from `defaultRPCOptions`, and the five `const { adapter: _adapter, ...options } = await loadRPCConfig()` workarounds in `src/*/helpers.ts` and the example servers — a phantom field that every consumer had to strip out by hand before the middleware would accept the config. `configuration.md` no longer lists it.
60
+ Compile-time only: a config file still carrying `adapter:` now fails typecheck rather than being ignored.
61
+
62
+ - **Also removed (types only): `rpcPrefix: false`.**
63
+
64
+ `MiddlewareOptions.rpcPrefix` was declared `string | false`, but `false` was **documented nowhere, tested nowhere, and handled nowhere**. Measured: it was byte-identical to omitting the option — `createRPCMiddleware({ rpcPrefix: false })` dispatched exactly like `createRPCMiddleware({})` for both `/__rpc/greet` (200) and `/anything/greet` (fall-through). A silent no-op that reads like "disable prefix gating" is worse than an absent option, so the type now says `string`. `resolveRPCPrefix()`'s signature was narrowed to match.
65
+ This is a **compile-time-only** change: JavaScript callers passing `false` are unaffected at runtime, since the `||` chain still falls through to the global prefix and then the default. It only surfaces for TypeScript users who wrote `rpcPrefix: false` — and for them the error is the correction, because they had been running with full prefix gating while believing the gate was disabled.
66
+
67
+ ### ⚠️ Behaviour change — malformed request bodies are now `400`, not `200` or `500`
68
+
69
+ - **A declared-JSON body that does not parse used to be answered `200` with the raw string handed to the handler** on Express, Koa and Fastify, and `500` on Hono and h3. Both outcomes were wrong, and the first was worse: it failed *open*. The cause was in `readBody`, where all three content-type branches fell through to a single `JSON.parse(body)` — so a `text/plain` body also went through `JSON.parse`, threw, and was "recovered" by a catch that resolved it as `text/plain`. A malformed JSON body and a legitimate text body were therefore indistinguishable, and the recovery path silently answered `200`.
70
+ Only a *declared* JSON body is now parsed strictly; everything else keeps the lenient sniff, which is deliberate and load-bearing — a request with no `Content-Type` at all (curl, and the nojs form fallback) that happens to carry JSON must still arrive parsed. On failure the error is tagged with a `400` (`httpError`, new) and every adapter answers `{ error: "Bad Request" }`.
71
+ This matches every supported host: Express `body-parser` (`entity.parse.failed` → 400), Fastify (`FST_ERR_CTP_INVALID_JSON_BODY` → 400), koa-bodyparser (400), and h3's own `readBody` (`HTTPError` 400, which rpc was discarding by parsing the body itself). Hono has no opinion here — its maintainers declined to own the case in honojs/hono#578 — so as the thing doing the parsing, rpc answers 400 for it.
72
+ - **Only the first scan in a process ran.** `scanForServerFiles` memoized with a single module-level boolean, so the *first* scan suppressed every subsequent one. The scan target is now keyed on the resolved `(scanRoot, serverFiles, rpcPrefix)` triple — everything that determines which files are read and where prefix-less functions register — so a repeat of the same scan is still skipped, but a second RPC instance on its own prefix now scans. Previously that instance asked for a lazy scan, got an early return, and answered `404` for every function it owned. Only reachable with a function declaring no `rpcPrefix` under a second middleware, i.e. a configuration that was already broken; it is now correct. Functions that declare their own prefix were never affected — one scan registers them all.
73
+ - **A malformed GET `?args=` is now `400` on all five adapters.** It previously threw out of the dispatch's `try` and was reported as a `500` — a cheap way for a client to generate server errors. The existing non-array case already answered `400`; only the malformed-syntax half was wrong.
74
+ - Client-error statuses are surfaced through one shared rule (`isClientHttpError` / `clientErrorStatus` / `clientErrorMessage` in `src/server-helpers.ts`) rather than per-adapter logic, so a host framework's signal and an rpc-raised one are handled identically. It reads both `status` (h3's `HTTPError`, rpc's `httpError`) and `statusCode` (the `http-errors` objects Express throws, Koa's `ctx.throw`), and the response body comes from a fixed table so no host-framework message is echoed back. `5xx` and unrecognised errors still go through `formatError`.
75
+
76
+ ### Fixed
77
+
78
+ - **⚠️ Hono's RPC was broken on every runtime that is not `@hono/node-server`.** `readBody`, the client-disconnect hook and `viteMiddleware` each read `c.env` unguarded, so on Cloudflare Workers, Bun, Deno, standalone serverless adapters — and Hono's own `app.fetch()` — `c.env` is `undefined` and *every* request threw a `TypeError` and came back as a `500`, including a well-formed one. The disconnect hook even carried a comment saying the runtime adapter "may be absent ... so guard the close hook" and then wrote `c.env.incoming?.`, which guards a null `incoming` but not an undefined `c.env`. The existing fixtures always set `env`, so nothing caught it; four tests now drive a real `Hono` app through `app.fetch()` where `c.env` is genuinely absent.
79
+
80
+ - **h3 reported an oversized body as a `500` instead of a `413`.** h3's `bodyLimit`/`assertBodySize` enforces its cap in two places: an honest `Content-Length` over the limit is rejected up front, before rpc is reached, but a **chunked** body (no length to check) trips *while the stream is read* — which is inside the RPC dispatch's `try` block. The adapter's generic catch flattened h3's `413` into a `500`, so h3 was the only adapter that reported an oversize body as a server fault; the other four get their `413` from the host body parser before rpc runs. The h3 adapter now forwards h3's own `4xx` out of the dispatch `try` (`413` becomes `{ error: "Payload Too Large" }`), while `5xx` and unrecognised errors still go through `formatError`. Covered by a real-`H3` test that streams a body with no `Content-Length` and asserts `413`, verified to fail with the fix removed.
81
+
82
+ - **`loadRPCConfig({ silent: true })` silently discarded the config** (`src/index.ts`). The documented single-argument form passes the options bag where the signature expects a config path, so `resolve()` threw `ERR_INVALID_ARG_TYPE`, the `catch` swallowed it, and the call returned `defaultRPCOptions` instead of the project's real config — with only a `Failed to load RPC config` warning as evidence. This was the form shown in `wiki/configuration.md`. An object first argument is now normalised to the options bag, and both the two-argument and one-argument forms work.
83
+ - **Vite 10+ silently used the wrong transform pipeline** (`src/index.ts`). `isOxc` was derived from `Number(viteVersion[0]) >= 8`, which reads the first *character*: `"10.4.2"` yields `1`, fails the `>= 8` test, and routes every future double-digit Vite through `transformWithEsbuild` instead of `transformWithOxc`. Now parsed as an integer major.
84
+ - **An export-less module silently abandoned the rest of the scan** (`src/scanForServerFiles.ts`). A module with no exports warned and then `return`ed out of the whole function rather than `continue`ing to the next file, so in `serverFiles: "glob"` mode every file after it was dropped without a word. Any glob project with a helper or barrel file that exports nothing was affected, and the missing functions surfaced only as `404`s at request time.
85
+ - **h3's outer gate resolved its prefix differently from the other four adapters** (`src/h3/createMiddleware.ts`). It used the three-way `rpcPrefix || getGlobalPrefix() || defaultPrefix` where express/fastify/hono/koa use `rpcPrefix ?? defaultPrefix`, so the same setup could pass h3's gate and be rejected by the others. Normalised to match.
86
+ - **Scan fixtures could not compile** (`tests/fixtures/scan-api/src/api/users.server.ts`, `upload.server.mts`): both called `createServerFunction(handler)` with the required `name` argument missing, so neither file was valid against the real signature. This never surfaced because `scanForServerFiles` discovers server modules by *filename* and never imports them — a fixture that would fail to compile sat in the tree indefinitely. Both now match the real API.
87
+
88
+ ### Tests
89
+
90
+ - **Regression tests for each runtime fix**, each verified to fail with the bug reintroduced. The Vite-version test asserts *which* transformer was called — the existing vite 7/8 tests only asserted the output was non-empty, and both mocked transformers return the input, which is why `Number(viteVersion[0])` survived. The scan test uses a fixture with two export-less modules and one populated, so it fails under any directory read order.
91
+ - New `tests/fixtures/scan-mixed/` (`a-empty.server.ts`, `b-loaded.server.ts`, `c-empty.server.ts`); `tests/fixtures/vite-mock.ts` gains `mockPlugin10Context` for the double-digit major version.
92
+ - **The Fastify reply mock now carries `redirect`** (`tests/fixtures/fastify.ts`): the v5-signature helper in `src/fastify/helpers.ts` calls `reply.redirect(location, status)`, so all four redirect tests had to attach the method to the double with a cast. The mock ships it instead. All four still assert on the call, so none became vacuous.
93
+ - Removed genuinely dead code from the suites: an unused `sendResponse` import (express, hono), an unused `app` binding (express), three unused `result` bindings (hono), an unused trailing `cb` parameter (h3), and a commented-out `origWarn` (scan).
94
+ - **17 pre-existing type errors in the test suite fixed.** None were reachable before — see the `check:tests` entry below.
95
+ - The `F1` finding was originally tracked as an `it.todo` in `tests/express.test.ts`; once fixed it became a six-test `global-prefix dispatch` block that sets a real global prefix. Three of the six fail with the bug reintroduced; the other three guard behaviour that must *not* change (default prefix with no global prefix set, explicit prefix winning over the global one, and boundary safety on a resolved prefix). `tests/plugin.test.ts` gained a file-level `afterEach` resetting the global prefix, since `loadRPCConfig` now publishes it and would otherwise leak into every later `createServerFunction` call.
96
+
97
+ ### Chores
98
+
99
+ - **CI now builds before testing** (`.github/workflows/ci.yml`). `tests/adapter-exports.test.ts` is a build-output test that parses the emitted `dist/<adapter>/*.d.mts`, but the build step was commented out. Because `dist/` is committed the file exists on a fresh checkout, so the suite silently validated the *last committed bundle* rather than current source — demonstrated by deleting `ExpressApp` from `src/express/types.d.ts`, which passed 56/56 with no build and fails with one. That test exists to stop a type name "quietly disappearing in a refactor", and it could not do so.
100
+ - **CI checks formatting instead of rewriting it** (`format:check`, new script). The workflow ran `pnpm format`, which rewrites files in the runner with no `git diff --exit-code` afterward, so format drift never failed a build — it just got silently "fixed" somewhere nobody would notice. `deno fmt --check` exits non-zero with `Found N not formatted files`.
101
+ - **The test suite is now typechecked** (`tsconfig.tests.json`, `check:tests`): `tsc` only ever covered `src`, so none of the test files were typechecked despite being a large part of the tree. `lint` now runs a third step after `check:ts`.
102
+ It is a **separate tsconfig on purpose**: `tsdown` emits declarations from `tsconfig.json`, and pulling `tests/fixtures` into that program raises TS2883 (*"inferred type cannot be named without a reference to 'Procedure' from `…/vitest/dist/…`"*) and fails the build. Under `noEmit` those errors do not occur, so tests are checked in their own program rather than the emit one.
103
+ - Removed dead and duplicated logic in the plugin entry: the unreachable third clause of the `transform` guard (`code.includes(...)` was already tested, and `typeof process === "undefined"` is always false inside a Node-hosted plugin), a per-call `await import("vite")` replaced by a static namespace import, a `(!initialCfg && !devServer) || !initialCfg` no-op, and the two duplicated config-merge blocks folded into one `mergeLoaded` helper.
104
+
105
+ ### Docs
106
+
107
+ - **`wiki/comparison.md` re-verified against vendor source and documentation**, and it was wrong in a security-relevant direction. Next.js does **not** abort a request with no `Origin` — an absent header is let through with a dev warning, on the stated reasoning that a handcrafted request cannot carry unwilling victim credentials. That is the *same* fail-open posture `@thednp/rpc` takes for its curl/native hole, and the page had claimed rpc was uniquely permissive about it. TanStack was also overstated: it rejects a request carrying *no* signal at all, not specifically one lacking `Origin` (a no-`Origin` request with a same-origin `Referer` is allowed). Corrected, with a note recording what the earlier draft got wrong.
108
+ Also updated: the `GHSA-mq59-m269-xvcx` entry with its CVE alias, severity, affected range (`16.0.1`–`16.1.6`) and fix version (`16.1.7`); SvelteKit's `trustedOrigins: ['*']`, which does **not** rescue a missing-`Origin` form POST, and the fact that PR #14795 was **closed unmerged** — remote functions are exempt from `trustedOrigins` by design, not awaiting a fix; a **removed** claim that `Referrer-Policy: no-referrer` causes SvelteKit CSRF false positives (SvelteKit reads only `Origin` and has no `Referer` fallback, so that mechanism does not exist); tRPC's POST `Content-Type` enforcement as a form-CSRF mitigation it does ship; `shield()` being dev-off by default; and verified versions for all five projects in the header and Sources.
109
+
110
+ ### Docs
111
+
112
+ - **`AGENTS.md` — the URL-normalization note overclaimed.** It stated that *every* adapter parses the request URL through the shared `safeURL()` helper. In fact fastify/hono/koa call it directly, express reaches it through `getRequestDetails`, and **h3 does not call it at all** — it reads `event.url.pathname`, which h3 has already parsed, so the throw-on-malformed-target case is handled upstream by h3. There was never a security gap; the note claimed more than the code did. Corrected in both the security section and the threat-model table.
113
+ - **`AGENTS.md` — the deferred-findings section became a *Fixed in 0.3.7* section** once the two prefix bugs landed, so it now documents the guardrails instead of the open questions: `resolveRPCPrefix()` is the single resolution point and a local `a || b || c` in an adapter is a regression; the removed default injection is *why* the `|| getGlobalPrefix()` fallback was unreachable; the boundary regex must be built from the resolved prefix but still gated on an explicitly-supplied one, because building it unconditionally would start gating a bare `createMiddleware({ path, handler })`; and a throwing config file still resets the `RPCConfig` cache, deliberately. It also keeps the coverage lesson that let both bugs through at 100%.
114
+ - **`wiki/middleware.md` — the framework-agnostic alternative to the `locals` bridge.** The page documented per-adapter recipes for reaching pre-dispatch framework state, but on Fastify and Hono those are five separate code paths with no coverage, and Fastify has no per-request store to bridge at all. New *The Framework-Agnostic Alternative: Handler Wrappers* section documents the part of the contract that actually carries: `event.locals` is a mutable object rpc passes into the request context, so a handler wrapper can resolve per-request data and store it there — identically on all five adapters, with no framework imports and no new rpc API. Includes a signed-cookie session worked example, a "when to use which" split against the framework-middleware path, and the honest tradeoff (a wrapper runs per function call, so request-pipeline concerns still belong in real framework middleware). The Fastify/Hono note now points at it as the better default
115
+ - **New `wiki/comparison.md`** — the cross-origin / CSRF boundary compared against Next.js Server Actions, TanStack Start, SvelteKit, and tRPC, with a section covering Vike and Telefunc. Verified against vendor documentation on 2026-09-27 rather than written from memory, which changed several conclusions: TanStack's `createCsrfMiddleware` is auto-installed and **fails closed**; Next.js shipped `GHSA-mq59-m269-xvcx` for treating `origin: null` as missing (rpc rejects it by default); tRPC shipped `CVE-2025-68130` for prototype pollution via FormData keys (rpc's urlencoded path is verified immune). Measured against TanStack's documented algorithm, rpc is **stricter in three cases** — an untrusted `Origin` claiming `Sec-Fetch-Site: same-origin`, `Origin: null`, and lookalike hosts — because checking `Origin` first is what closes them, and more permissive in two, both deliberate (the allowlisted sibling, and the headerless curl case)
116
+ - Framed as *fail-open by default, strictest-once-configured, multi-origin without a proxy* — with `Where the trade costs you` keeping the real sharp edges (case-sensitive origin matching, literal origins only, no automatic input validation, deliberate `Referer` omission). Linked from all 13 other wiki pages, `wiki/index.md`, `AGENTS.md`, and `llms.txt`; the `AGENTS.md` entry warns future agents to read it before writing security copy
117
+ - Two broken wiki anchors fixed: `client-usage.md#native-http-clients--unwrapenvelopet` and the tier-order slug in `comparison.md`. A link/anchor pass validates all 47.
118
+
119
+ ## [0.3.6] - 2026-09-27
120
+
121
+ Two independent changes: a **behaviour change** to origin validation, and an
122
+ additive expansion of the adapter type exports.
123
+
124
+ ### ⚠️ Behaviour change — `Sec-Fetch-Site` fallback under `origin`
125
+
126
+ - **`isOriginRequestAllowed(allowed, origin, site)`** (`src/server-helpers.ts`, exported from `@thednp/rpc/server`): the origin check now consults `Sec-Fetch-Site` when `Origin` is missing, instead of treating a headerless request as an unchecked pass. Previously anything in the chain that stripped `Origin` — a proxy, a sanitising middleware, a misconfigured CDN — turned a precise allowlist check into a **no-op**. Four tiers, first signal wins:
127
+ 1. `origin` option unset → everything passes (unchanged; the check is opt-in)
128
+ 2. `Origin` present → the allowlist decides, exactly as before
129
+ 3. `Origin` absent, `Sec-Fetch-Site` present → allow only `same-origin` and `none`; anything else, including an unrecognised value, is **403**
130
+ 4. Both absent → passes. This is the deliberate, documented curl/native-client hole and it stays
131
+ - **All five adapters now pass both headers.** `Origin` still short-circuits ahead of `Sec-Fetch-Site`, and it has to: the allowlist exists precisely to admit a sibling subdomain, whose browser request carries `Sec-Fetch-Site: same-site` — a value tier 3 alone would reject. `Sec-Fetch-Site` is a coarse enum that cannot name a host, so it only earns a vote once the precise signal is gone, at which point it fails closed. A regression test pins the ordering
132
+ - **What changes:** only requests that (a) lack `Origin`, (b) carry `Sec-Fetch-Site`, and (c) claim anything other than `same-origin`/`none` move from *pass* to *403*. That class was the bug. Browsers never strip `Origin` themselves, so tier 3 only fires when something removed it — **no legitimate browser request can regress**
133
+ - **`isOriginAllowed` is unchanged and still exported** (public API since 0.3.5); it is now an internal building block
134
+ - **No new option.** Setting `origin` remains the only opt-in, so the surface is frozen — the plan's stated default. An app with `origin` set gets the stronger check by upgrading, with no code change
135
+ - **Empty header values count as absent.** Adapters disagree on what their accessor returns for a missing header (Node yields `undefined`, Hono's `c.req.header()` yields `""`), and an empty `Sec-Fetch-Site` is not a real browser signal. Normalising in the shared helper keeps all five behaving identically instead of inheriting each framework's convention
136
+ - `origin` JSDoc in `MiddlewareOptions` rewritten — it no longer promises unconditional headerless passthrough
137
+
138
+ ### Added — adapter type exports for wrappers
139
+
140
+ Wrapper libraries must be able to annotate apps, requests, responses, `next` functions and middleware **without depending on the framework**. The 0.3.2 re-exports were incomplete and irregular, so each adapter exposed a different shape. Now uniform and additive:
141
+
142
+ | Adapter | App (new) | Request (new) | Response (new) | `next` (new) |
143
+ | -------- | ----------- | ------------------------- | ----------------------------- | ---------------------------------- |
144
+ | express | `ExpressApp` | — (`ExpressRequest`) | — (`ExpressResponse`) | — (`ExpressNext`) |
145
+ | fastify | `FastifyApp` | — (`FastifyRequest`) | `FastifyResponse` | `FastifyNext` |
146
+ | hono | `HonoApp` | `HonoRequest` | `HonoResponse` | `HonoNext` |
147
+ | koa | `KoaApp` | `KoaRequest` | `KoaResponse` | — (`KoaNext`) |
148
+ | h3 | — (`H3App`) | `H3Request` | `H3Response` | `H3Next` |
149
+
150
+ - **`next` was missing on 3 of 5 adapters** — `createMiddleware` accepts a `HookHandlerDoneFunction` (Fastify), a `Next` (Hono) and a `next` callback (h3), and none of those types were exported, so a wrapper could not type a composed handler
151
+ - **Hono had no request type.** Only `HonoContext` was exported, forcing consumers to cast the request object structurally
152
+ - **`RequestDetails` / `ResponseDetails` were express-only** despite containing no Express-specific types. They now live in `src/adapter-types.ts` and are re-exported from **all five** adapters, so one helper can be written across all frameworks. Still importable from `@thednp/rpc/express`
153
+ - **`Koa` re-export moved** from `src/koa/index.ts` into `src/koa/types.d.ts` beside its siblings (it is the only type that lived in a barrel). `src/koa/helpers.ts` now takes the type from `types.d.ts` instead of the barrel, removing an index→helpers import cycle
154
+ - **Purely additive.** No existing export renamed, removed or narrowed — `Express`, `Fastify`, `Hono`, `Koa`, `H3Event`, `HonoContext`, `KoaContext` all keep working. Type-only, so emitted `.mjs` is unchanged
155
+
156
+ ### Tests
157
+
158
+ - **Adapter export-surface contract suite** (`tests/adapter-exports.test.ts`): asserts every adapter's emitted declaration exports the full required name set, plus a back-compat assertion for the pre-0.3.6 names. Type-only exports are erased from the `.mjs`, so the suite parses `dist/<adapter>/<adapter>.d.mts` — the thing a consumer actually resolves. Verified non-vacuous: renaming any required name in a copy makes it fail
159
+ - `isOriginRequestAllowed`: every row of the behaviour matrix verbatim, the sibling-subdomain ordering regression, the empty/whitespace-value normalisation, and case-sensitivity
160
+ - Per adapter: `Sec-Fetch-Site: cross-site` with no `Origin` → `403`, `same-origin` → `200`, and the sibling-subdomain regression (`allowlisted Origin` + `same-site` → `200`)
161
+ - Fixed a fidelity bug in the Hono test fixture — `header()` returned `""` for a missing header where real Hono returns `undefined`, which had masked the empty-value normalisation
162
+ - **556 tests, 100% on all metrics**
163
+
164
+ ### Chores
165
+
166
+ - Bump version to `0.3.6` (`package.json`, `deno.json`)
167
+
3
168
  ## [0.3.5] - 2026-09-26
4
169
 
5
170
  ### Features
package/README.md CHANGED
@@ -172,7 +172,6 @@ Create `rpc.config.ts` at your project root:
172
172
  import { defineConfig } from "@thednp/rpc/config";
173
173
 
174
174
  export default defineConfig({
175
- adapter: "express",
176
175
  rpcPrefix: "__rpc",
177
176
  });
178
177
  ```
@@ -262,7 +261,7 @@ pnpm test:watch # Run tests in watch mode with coverage
262
261
  pnpm test:ui # Run tests with UI
263
262
  ```
264
263
 
265
- Tests use **Vitest** with **Istanbul** coverage — 10 test files covering the plugin, scanning, server/client helpers, request context, and all five adapters, at 100% coverage.
264
+ Tests use **Vitest** with **Istanbul** coverage — 11 test files covering the plugin, scanning, server/client helpers, request context, the adapter type-export surface, and all five adapters, at 100% coverage.
266
265
 
267
266
  ### Live Testing
268
267
 
@@ -353,6 +352,7 @@ The full threat model, including edge cases and configuration options for tighte
353
352
  - [Client Usage](./wiki/client-usage.md) — Client-side usage
354
353
  - [Wire Protocol](./wiki/wire-protocol.md) — The HTTP contract behind the generated clients (curl debugging)
355
354
  - [Adapters](./wiki/adapters.md) — Framework adapters
355
+ - [Comparison](./wiki/comparison.md) — How the cross-origin/CSRF boundary compares to Next.js Server Actions, TanStack Start, SvelteKit, and tRPC
356
356
  - [Best Practices](./wiki/best-practices.md) — Tips and best practices
357
357
  - [Security](./wiki/security.md) — Security hardening
358
358
 
@@ -28,13 +28,6 @@ interface RpcPluginOptions {
28
28
  * rpcPrefix: "api/rpc"
29
29
  */
30
30
  rpcPrefix: "__rpc" | string;
31
- /**
32
- * Option to set an adapter for the middleware connection. The default is _express_,
33
- * which is the most popular and battle tested server app. The _express_ adapter is
34
- * also compatible with the vite's Connect development server.
35
- * @default express
36
- */
37
- adapter: "express" | "hono" | "h3" | "fastify" | "koa";
38
31
  /**
39
32
  * Root directory from which the plugin scans for server files.
40
33
  * Defaults to `<root>/src/api`. Use this in monorepos where server files
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.mts","names":[],"sources":["../../src/types.d.ts","../../src/config.ts"],"mappings":";;;;;;;;;;;;;;;;;;UAsPiB;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;;;;;;;;;;qBC/QW,eACX,GAAG,QAAQ,sBACR"}
1
+ {"version":3,"file":"config.d.mts","names":[],"sources":["../../src/types.d.ts","../../src/config.ts"],"mappings":";;;;;;;;;;;;;;;;;;UAmQiB;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;;;;;;;;;;qBCpRW,eACX,GAAG,QAAQ,sBACR"}
@@ -1,6 +1,10 @@
1
+ /**
2
+ * Baseline plugin options. `defineConfig` merges a user's partial config over
3
+ * these, and `loadRPCConfig` merges a loaded config file over them, so every
4
+ * option has a defined value even when a config file omits it.
5
+ */
1
6
  const defaultRPCOptions = {
2
7
  rpcPrefix: "__rpc",
3
- adapter: "express",
4
8
  serverFiles: "exact",
5
9
  scanRoot: void 0
6
10
  };
@@ -1 +1 @@
1
- {"version":3,"file":"config.mjs","names":[],"sources":["../../src/options.ts","../../src/config.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\nexport const defaultServerFnOptions: ServerFunctionOptions = {\n contentType: \"application/json\",\n credentials: \"same-origin\",\n method: \"POST\",\n};\n\nexport const defaultPrefix = \"__rpc\";\n\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: defaultPrefix,\n adapter: \"express\",\n serverFiles: \"exact\",\n scanRoot: undefined,\n};\n\nexport const defaultMiddlewareOptions: MiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n};\n","/**\n * Vite-free configuration helpers.\n *\n * This module intentionally has zero runtime dependencies (not even on `vite`),\n * so `rpc.config.ts` files that import it stay safe to load in serverless\n * bundles where Vite is not installed. Importing the main plugin entry\n * (`@thednp/rpc`) instead would drag Vite into every server-side consumer.\n */\nimport type { RpcPluginOptions } from \"./types.d.ts\";\nimport { defaultRPCOptions } from \"./options.ts\";\n\n/**\n * Type-safe helper to create an RPC configuration object.\n * Merges the provided partial config over the built-in defaults,\n * skipping explicitly `undefined` values.\n * @param uniConfig - System-wide RPC configuration overrides\n * @returns Complete RPC plugin options with defaults applied\n */\nexport const defineConfig: (\n c: Partial<RpcPluginOptions>,\n) => RpcPluginOptions = (uniConfig: Partial<RpcPluginOptions>) => {\n const merged: RpcPluginOptions & Record<string, unknown> = {\n ...defaultRPCOptions,\n };\n for (const [key, value] of Object.entries(uniConfig)) {\n // istanbul ignore else\n if (value !== undefined) {\n merged[key] = value;\n }\n }\n return merged;\n};\n"],"mappings":"AAcA,MAAa,oBAAsC;CACjD,WAAW;CACX,SAAS;CACT,aAAa;CACb,UAAU,KAAA;AACZ;;;;;;;;;;ACDA,MAAa,gBAEY,cAAyC;CAChE,MAAM,SAAqD,EACzD,GAAG,kBACL;CACA,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS,GAEjD,IAAI,UAAU,KAAA,GACZ,OAAO,OAAO;CAGlB,OAAO;AACT"}
1
+ {"version":3,"file":"config.mjs","names":[],"sources":["../../src/options.ts","../../src/config.ts"],"sourcesContent":["import type {\n MiddlewareOptions,\n RpcPluginOptions,\n ServerFunctionOptions,\n} from \"./types.d.ts\";\n\n/**\n * Defaults applied to a server function that declares no `method`,\n * `credentials`, or `contentType` of its own.\n */\nexport const defaultServerFnOptions: ServerFunctionOptions = {\n contentType: \"application/json\",\n credentials: \"same-origin\",\n method: \"POST\",\n};\n\n/**\n * The built-in RPC endpoint prefix, used when neither an explicit prefix nor a\n * global one (`getGlobalPrefix`) is supplied. Kept for backward compatibility\n * with pre-multi-prefix setups, where every function lived under this one map.\n */\nexport const defaultPrefix = \"__rpc\";\n\n/**\n * Baseline plugin options. `defineConfig` merges a user's partial config over\n * these, and `loadRPCConfig` merges a loaded config file over them, so every\n * option has a defined value even when a config file omits it.\n */\nexport const defaultRPCOptions: RpcPluginOptions = {\n rpcPrefix: defaultPrefix,\n serverFiles: \"exact\",\n scanRoot: undefined,\n};\n\n/**\n * Baseline middleware options. Note `rpcPrefix` is `undefined` rather than\n * `defaultPrefix` on purpose: leaving it unset lets `resolveRPCPrefix` fall\n * through to the global prefix, which is what makes a published global prefix\n * reach the middleware.\n */\nexport const defaultMiddlewareOptions: MiddlewareOptions = {\n rpcPrefix: undefined,\n path: undefined,\n origin: undefined,\n};\n","/**\n * Vite-free configuration helpers.\n *\n * This module intentionally has zero runtime dependencies (not even on `vite`),\n * so `rpc.config.ts` files that import it stay safe to load in serverless\n * bundles where Vite is not installed. Importing the main plugin entry\n * (`@thednp/rpc`) instead would drag Vite into every server-side consumer.\n */\nimport type { RpcPluginOptions } from \"./types.d.ts\";\nimport { defaultRPCOptions } from \"./options.ts\";\n\n/**\n * Type-safe helper to create an RPC configuration object.\n * Merges the provided partial config over the built-in defaults,\n * skipping explicitly `undefined` values.\n * @param uniConfig - System-wide RPC configuration overrides\n * @returns Complete RPC plugin options with defaults applied\n */\nexport const defineConfig: (\n c: Partial<RpcPluginOptions>,\n) => RpcPluginOptions = (uniConfig: Partial<RpcPluginOptions>) => {\n const merged: RpcPluginOptions & Record<string, unknown> = {\n ...defaultRPCOptions,\n };\n for (const [key, value] of Object.entries(uniConfig)) {\n // istanbul ignore else\n if (value !== undefined) {\n merged[key] = value;\n }\n }\n return merged;\n};\n"],"mappings":";;;;;AA4BA,MAAa,oBAAsC;CACjD,WAAW;CACX,aAAa;CACb,UAAU,KAAA;AACZ;;;;;;;;;;ACdA,MAAa,gBAEY,cAAyC;CAChE,MAAM,SAAqD,EACzD,GAAG,kBACL;CACA,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS,GAEjD,IAAI,UAAU,KAAA,GACZ,OAAO,OAAO;CAGlB,OAAO;AACT"}
@@ -1,32 +1,44 @@
1
1
  import { Connect, ViteDevServer } from "vite";
2
- import { BodyResult, JsonValue, MiddlewareOptions, RpcPluginOptions } from "@thednp/rpc";
2
+ import { AdapterName, BodyResult, MiddlewareOptions } from "@thednp/rpc";
3
3
  import { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
4
- import { Express, Express as Express$1, NextFunction, NextFunction as ExpressNext, Request, Request as ExpressRequest, Response, Response as ExpressResponse } from "express";
5
- //#region src/express/types.d.ts
4
+ import { Express, Express as Express$1, Express as ExpressApp, NextFunction, NextFunction as ExpressNext, Request, Request as ExpressRequest, Response as ExpressResponse, Response as Response$1 } from "express";
5
+ import "hono";
6
+ import "@hono/node-server";
7
+ import "hono/utils/http-status";
8
+ import "hono/factory";
9
+ import "fastify";
10
+ import "fastify-plugin";
11
+ import "koa";
12
+ import "h3";
13
+ //#region src/types.d.ts
14
+ // primitives and their compositions
15
+ /**
16
+ * Primitive JSON values, including `undefined` for optional parameters.
17
+ */
18
+ type JsonPrimitive = string | number | boolean | null | undefined;
19
+ /**
20
+ * A JSON object whose values are JSON values or arrays.
21
+ */
22
+ type JsonObject = {
23
+ [key: string]: JsonValue | JsonArray;
24
+ };
6
25
  /**
7
- * Express-specific middleware options, constrained to the `"express"` adapter.
26
+ * A JSON array of JSON values.
8
27
  */
9
- type ExpressMiddlewareOptions = MiddlewareOptions<"express">;
28
+ type JsonArray = (FormData | JsonValue)[];
10
29
  /**
11
- * Express middleware factory: takes optional initial options and returns
12
- * the Express/Connect-compatible handler.
30
+ * Any JSON-serializable value: primitive, array, or object.
13
31
  */
14
- type ExpressMiddlewareFn = <A extends RpcPluginOptions["adapter"] = "express">(initialOptions?: Partial<ExpressMiddlewareOptions>) => ExpressMiddlewareHooks["handler"];
15
- /**
16
- * Express/Connect middleware handler signature used by the RPC middleware.
17
- */
18
- interface ExpressMiddlewareHooks {
19
- /**
20
- * The handler invoked for each matched request.
21
- * @param req - Node or Express request object
22
- * @param res - Node or Express response object
23
- * @param next - Connect or Express next function
24
- */
25
- handler: (req: IncomingMessage | Request, res: ServerResponse | Response, next: Connect.NextFunction | NextFunction) => Promise<void>;
26
- }
32
+ type JsonValue = JsonPrimitive | JsonArray | JsonObject;
33
+ //#endregion
34
+ //#region src/adapter-types.d.ts
27
35
  /**
28
36
  * Wraps a server response to normalize status, header, and send operations
29
- * across Node `ServerResponse` and Express `Response` objects.
37
+ * across Node `ServerResponse` and framework response objects.
38
+ *
39
+ * Re-exported from every adapter (`@thednp/rpc/express`, `/fastify`, `/hono`,
40
+ * `/koa`, `/h3`) so a consumer can name the shape without importing from the
41
+ * express adapter specifically.
30
42
  */
31
43
  type ResponseDetails = {
32
44
  /** Whether the response was already sent */
@@ -42,6 +54,8 @@ type ResponseDetails = {
42
54
  };
43
55
  /**
44
56
  * Normalized view of an incoming request: URL parts, headers, and method.
57
+ *
58
+ * Re-exported from every adapter, for the same reason as {@link ResponseDetails}.
45
59
  */
46
60
  type RequestDetails = {
47
61
  /** Full request URL (path + query string) */
@@ -56,6 +70,29 @@ type RequestDetails = {
56
70
  method: string | undefined;
57
71
  };
58
72
  //#endregion
73
+ //#region src/express/types.d.ts
74
+ /**
75
+ * Express-specific middleware options, constrained to the `"express"` adapter.
76
+ */
77
+ type ExpressMiddlewareOptions = MiddlewareOptions<"express">;
78
+ /**
79
+ * Express middleware factory: takes optional initial options and returns
80
+ * the Express/Connect-compatible handler.
81
+ */
82
+ type ExpressMiddlewareFn = <A extends AdapterName = "express">(initialOptions?: Partial<ExpressMiddlewareOptions>) => ExpressMiddlewareHooks["handler"];
83
+ /**
84
+ * Express/Connect middleware handler signature used by the RPC middleware.
85
+ */
86
+ interface ExpressMiddlewareHooks {
87
+ /**
88
+ * The handler invoked for each matched request.
89
+ * @param req - Node or Express request object
90
+ * @param res - Node or Express response object
91
+ * @param next - Connect or Express next function
92
+ */
93
+ handler: (req: IncomingMessage | Request, res: ServerResponse | Response$1, next: Connect.NextFunction | NextFunction) => Promise<void>;
94
+ }
95
+ //#endregion
59
96
  //#region src/express/createMiddleware.d.ts
60
97
  /**
61
98
  * Creates an Express middleware with optional path and rpcPrefix filtering.
@@ -89,13 +126,6 @@ export declare function attachRPC(app: Express$1): Promise<void>;
89
126
  * @param vite - Running Vite dev server
90
127
  */
91
128
  export declare function attachVite(app: Express$1, vite: ViteDevServer): void;
92
- /**
93
- * Reads and parses the HTTP request body from an Express or Node IncomingMessage.
94
- * If a body parser middleware (e.g. express.json()) already consumed the stream,
95
- * uses the pre-parsed body from `req.body`.
96
- * @param req - Express or Node.js IncomingMessage
97
- * @returns A promise resolving to the parsed body with its content type
98
- */
99
129
  export declare const readBody: (req: Request | IncomingMessage) => Promise<BodyResult>;
100
130
  /**
101
131
  * Type guard that checks whether a request is an Express Request (has `originalUrl`).
@@ -108,7 +138,7 @@ export declare const isExpressRequest: (req: IncomingMessage | Request) => req i
108
138
  * @param res - A Node ServerResponse or Express Response
109
139
  * @returns True if the response is an Express Response
110
140
  */
111
- export declare const isExpressResponse: (res: ServerResponse | Response) => res is Response;
141
+ export declare const isExpressResponse: (res: ServerResponse | Response$1) => res is Response$1;
112
142
  /**
113
143
  * Issues an HTTP redirect on an Express or raw Node ServerResponse.
114
144
  * Uses Express's native `res.redirect(status, location)` when an Express
@@ -120,7 +150,7 @@ export declare const isExpressResponse: (res: ServerResponse | Response) => res
120
150
  * @param location - The URL to redirect to
121
151
  * @param status - HTTP status code, defaults to 303
122
152
  */
123
- export declare const redirect: (res: ServerResponse | Response, location: string, status?: number) => void;
153
+ export declare const redirect: (res: ServerResponse | Response$1, location: string, status?: number) => void;
124
154
  /**
125
155
  * Type guard that checks whether a request has a pre-parsed body (`body` property).
126
156
  * Used to detect if a body-parser middleware already consumed the stream.
@@ -141,7 +171,7 @@ export declare const getRequestDetails: (request: Request | IncomingMessage) =>
141
171
  * @param response - Express or Node.js server response object
142
172
  * @returns A ResponseDetails object with setHeader, setStatusCode, and sendResponse helpers
143
173
  */
144
- export declare const getResponseDetails: (response: Response | ServerResponse) => ResponseDetails;
174
+ export declare const getResponseDetails: (response: Response$1 | ServerResponse) => ResponseDetails;
145
175
  //#endregion
146
- export type { Express, ExpressMiddlewareFn, ExpressMiddlewareHooks, ExpressMiddlewareOptions, ExpressNext, ExpressRequest, ExpressResponse, RequestDetails, ResponseDetails };
176
+ export type { Express, ExpressApp, ExpressMiddlewareFn, ExpressMiddlewareHooks, ExpressMiddlewareOptions, ExpressNext, ExpressRequest, ExpressResponse, RequestDetails, ResponseDetails };
147
177
  //# sourceMappingURL=express.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"express.d.mts","names":["Express","ExpressRequest","ExpressResponse"],"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;;;;;KAM3B;;EAEV;;EAEA;;EAEA,cAAc;;EAEd,SAAS;;EAET;;;;;;;;;;;qBCpBW,kBAAkB;;;;;;;;;;qBAiFlB,qBAAqB;;;;;;;;wBCrHZ,UAAU,KAAKA,YAAO;;;;;;wBAc5B,WAAW,KAAKA,WAAS,MAAM;;;;;;;;qBAWlC,WAAQ,KACdC,UAAiB,oBACrB,QAAQ;;;;;;qBAqFE,mBAAgB,KACtB,kBAAkBA,YACtB,OAAOA;;;;;;qBASG,oBAAiB,KACvB,iBAAiBC,aACrB,OAAOA;;;;;;;;;;;;qBAeG,WAAQ,KACd,iBAAiBA,UAAe,kBACrB;;;;;;;qBAkBL,mBAAgB,KACtB,kBAAkBD,YACtB,OAAOA;;;;;;;qBAUG,oBAAiB,SACnBA,UAAiB,oBACzB;;;;;;;qBAqBU,qBAAkB,UACnBC,WAAkB,mBAC3B"}
1
+ {"version":3,"file":"express.d.mts","names":["Response","Express","ExpressRequest","ExpressResponse"],"sources":["../../src/types.d.ts","../../src/adapter-types.ts","../../src/express/types.d.ts","../../src/express/createMiddleware.ts","../../src/express/helpers.ts"],"mappings":";;;;;;;;;;;;;;;;;KAsIY;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,aAAa,WAAW;;;;KAIxB,YAAY,gBAAgB,YAAY;;;;;;;;;;;KCtIxC;;EAEV;;EAEA,YAAY,cAAc;;EAE1B;;EAEA,gBAAgB;;EAEhB,eAAe,cAAc,QAAQ;;;;;;;KAQ3B;;EAEV;;EAEA;;EAEA,cAAc;;EAEd,SAAS;;EAET;;;;;;;KCxBU,2BAA2B;;;;;KAM3B,uBACV,UAAU,yBAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiBA,YACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;;;;;;qBCYM,kBAAkB;;;;;;;;;;qBAsFlB,qBAAqB;;;;;;;;wBCzHZ,UAAU,KAAKC,YAAO;;;;;;wBAc5B,WAAW,KAAKA,WAAS,MAAM;qBA0BlC,WAAQ,KACdC,UAAiB,oBACrB,QAAQ;;;;;;qBAuGE,mBAAgB,KACtB,kBAAkBA,YACtB,OAAOA;;;;;;qBASG,oBAAiB,KACvB,iBAAiBC,eACrB,OAAOA;;;;;;;;;;;;qBAeG,WAAQ,KACd,iBAAiBA,YAAe,kBACrB;;;;;;;qBAkBL,mBAAgB,KACtB,kBAAkBD,YACtB,OAAOA;;;;;;;qBAUG,oBAAiB,SACnBA,UAAiB,oBACzB;;;;;;;qBAqBU,qBAAkB,UACnBC,aAAkB,mBAC3B"}