@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.
- package/AGENTS.md +39 -12
- package/CHANGELOG.md +165 -0
- package/README.md +2 -2
- package/dist/config/config.d.mts +0 -7
- package/dist/config/config.d.mts.map +1 -1
- package/dist/config/config.mjs +5 -1
- package/dist/config/config.mjs.map +1 -1
- package/dist/express/express.d.mts +62 -32
- package/dist/express/express.d.mts.map +1 -1
- package/dist/express/express.mjs +71 -23
- package/dist/express/express.mjs.map +1 -1
- package/dist/fastify/fastify.d.mts +81 -47
- package/dist/fastify/fastify.d.mts.map +1 -1
- package/dist/fastify/fastify.mjs +73 -23
- package/dist/fastify/fastify.mjs.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.d.mts +30 -55
- package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.mjs +72 -22
- package/dist/fastify/plugin/fastify/plugin.mjs.map +1 -1
- package/dist/h3/h3.d.mts +73 -4
- package/dist/h3/h3.d.mts.map +1 -1
- package/dist/h3/h3.mjs +69 -22
- package/dist/h3/h3.mjs.map +1 -1
- package/dist/helpers/helpers.d.mts.map +1 -1
- package/dist/helpers/helpers.mjs +2 -0
- package/dist/helpers/helpers.mjs.map +1 -1
- package/dist/hono/hono.d.mts +71 -6
- package/dist/hono/hono.d.mts.map +1 -1
- package/dist/hono/hono.mjs +94 -24
- package/dist/hono/hono.mjs.map +1 -1
- package/dist/index.d.mts +43 -25
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +62 -28
- package/dist/index.mjs.map +1 -1
- package/dist/koa/koa.d.mts +73 -11
- package/dist/koa/koa.d.mts.map +1 -1
- package/dist/koa/koa.mjs +75 -23
- package/dist/koa/koa.mjs.map +1 -1
- package/dist/server/server.d.mts +159 -18
- package/dist/server/server.d.mts.map +1 -1
- package/dist/server/server.mjs +186 -15
- package/dist/server/server.mjs.map +1 -1
- package/llms.txt +5 -3
- package/package.json +5 -3
- 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
|
|
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 `
|
|
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
|
|
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
|
|
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 `
|
|
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
|
-
- **
|
|
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()`
|
|
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/
|
|
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 —
|
|
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
|
|
package/dist/config/config.d.mts
CHANGED
|
@@ -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":";;;;;;;;;;;;;;;;;;
|
|
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"}
|
package/dist/config/config.mjs
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
|
5
|
-
|
|
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
|
-
*
|
|
26
|
+
* A JSON array of JSON values.
|
|
8
27
|
*/
|
|
9
|
-
type
|
|
28
|
+
type JsonArray = (FormData | JsonValue)[];
|
|
10
29
|
/**
|
|
11
|
-
*
|
|
12
|
-
* the Express/Connect-compatible handler.
|
|
30
|
+
* Any JSON-serializable value: primitive, array, or object.
|
|
13
31
|
*/
|
|
14
|
-
type
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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":"
|
|
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"}
|