@thednp/rpc 0.3.5 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -95,15 +95,18 @@ The tsdown.config.ts produces multiple entries:
95
95
  | ---------------------------------| --------------------------------------------------------------------| -----|
96
96
  | `tests/plugin.test.ts` | Plugin init, loadRPCConfig, createServerFunction, getClientModules | |
97
97
  | `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 | |
98
+ | `tests/server-helpers.test.ts` | RPCError, formatError, redirect, glob walking, origin allowlist (`isOriginAllowed`) and the four-tier `isOriginRequestAllowed` behaviour matrix | |
99
+ | `tests/client-helpers.test.ts` | Client fetch stubs, retrieval helpers, `unwrapEnvelope` error contract | |
100
100
  | `tests/context.test.ts` | provideRequestContext / getRequestContext (AsyncLocalStorage) | |
101
+ | `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
102
  | `tests/express.test.ts` | Express helpers, createMiddleware, createRPCMiddleware | |
102
103
  | `tests/fastify.test.ts` | Fastify helpers, plugin, createMiddleware, createRPCMiddleware | |
103
104
  | `tests/h3.test.ts` | h3 helpers, viteMiddleware, createMiddleware, createRPCMiddleware | |
104
105
  | `tests/hono.test.ts` | Hono helpers, createMiddleware, createRPCMiddleware | |
105
106
  | `tests/koa.test.ts` | Koa helpers, createMiddleware, createRPCMiddleware | |
106
107
 
108
+ Run `pnpm build` before `pnpm test` — `tests/adapter-exports.test.ts` reads the emitted declarations, and the adapters themselves import `@thednp/rpc/server` (aliased to `src/server.ts` under vitest, so only that one suite needs the build).
109
+
107
110
  ## Important Notes
108
111
 
109
112
  - 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
@@ -119,7 +122,7 @@ The tsdown.config.ts produces multiple entries:
119
122
  - Framework-agnostic core with adapters for Express, Fastify, Hono, Koa, and h3
120
123
  - Client modules are auto-generated with `AbortController` support for cancellation
121
124
  - 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`)
125
+ - **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
126
 
124
127
  ## Security & Hardening
125
128
 
@@ -133,7 +136,8 @@ The tsdown.config.ts produces multiple entries:
133
136
  - **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
137
  - **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
138
  - **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
139
+ - **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
140
+ - **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
141
  - **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
142
  - **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
143
  - **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
@@ -147,6 +151,7 @@ The framework's security boundary is the **RPC prefix-gated HTTP endpoint**. Inp
147
151
  | `rpcPrefix` (config) | `rpc.config.ts` / dev options | Developer-trusted | Escaped before regex; validated before code gen |
148
152
  | Function export name | `src/api/server.ts` exports | Developer-trusted | Validated against identifier regex before client codegen |
149
153
  | HTTP request URL | Untrusted client | Boundary-filtered | Prefix regex (escaped, anchored, hoisted); non-throwing `safeURL()` normalization |
154
+ | 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
155
  | HTTP request body | Untrusted client | Capped by framework | Framework body parsers cap JSON and raw bodies |
151
156
 
152
157
  **Attackers cannot**:
@@ -154,6 +159,8 @@ The framework's security boundary is the **RPC prefix-gated HTTP endpoint**. Inp
154
159
  - Bypass the prefix via segment-prefixing tricks (anchored regex, not `startsWith`)
155
160
  - Trigger ReDoS via prefix metacharacters (escaped before compilation)
156
161
  - Exhaust memory via large raw text bodies (framework body parsers enforce limits)
162
+ - Sneak past the origin allowlist with a lookalike host (`https://app.example.com.evil.com` is rejected — matching is exact, never a prefix test)
163
+ - 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
164
 
158
165
  **Attackers are expected to**:
159
166
  - Be free to send as many requests as the host allows (no rate limiting — host's responsibility)
@@ -172,10 +179,12 @@ The framework's security boundary is the **RPC prefix-gated HTTP endpoint**. Inp
172
179
  - `wiki/getting-started.md` — Installation, project structure, auto-scanning, and your first function
173
180
  - `wiki/configuration.md` — Configuration reference (`rpc.config.ts`, `vite.config.ts`, options)
174
181
  - `wiki/server-functions.md` — `createServerFunction` API, methods, validation, **request context (`getRequestContext`/`provideRequestContext`)** for per-request data access across async call stacks
182
+ - `wiki/multi-prefix-guide.md` — Parallel RPC instances: versioned/public/admin API layouts, per-prefix middleware, canary deployments, origin validation per instance
175
183
  - `wiki/middleware.md` — universal adapter-agnostic middleware via the request context (`locals` bridge, `getRequestMeta`, `sendResponse`, `functionName`)
176
184
  - `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
185
+ - `wiki/client-usage.md` — Client-side usage, type safety, react-query integration, native clients via `unwrapEnvelope`
178
186
  - `wiki/wire-protocol.md` — HTTP contract, request/response bodies, curl debugging
179
- - `wiki/adapters.md` — Framework adapters (Express, Fastify, Hono, Koa, h3)
187
+ - `wiki/adapters.md` — Framework adapters (Express, Fastify, Hono, Koa, h3) and the re-exported framework type contract
180
188
  - `wiki/security.md` — Security hardening
181
189
  - `wiki/best-practices.md` — Production patterns (auth, rate limiting, body limits, CSRF)
190
+ - `wiki/index.md` — Documentation index / table of contents
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.6] - 2026-09-27
4
+
5
+ Two independent changes: a **behaviour change** to origin validation, and an
6
+ additive expansion of the adapter type exports.
7
+
8
+ ### ⚠️ Behaviour change — `Sec-Fetch-Site` fallback under `origin`
9
+
10
+ - **`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:
11
+ 1. `origin` option unset → everything passes (unchanged; the check is opt-in)
12
+ 2. `Origin` present → the allowlist decides, exactly as before
13
+ 3. `Origin` absent, `Sec-Fetch-Site` present → allow only `same-origin` and `none`; anything else, including an unrecognised value, is **403**
14
+ 4. Both absent → passes. This is the deliberate, documented curl/native-client hole and it stays
15
+ - **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
16
+ - **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**
17
+ - **`isOriginAllowed` is unchanged and still exported** (public API since 0.3.5); it is now an internal building block
18
+ - **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
19
+ - **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
20
+ - `origin` JSDoc in `MiddlewareOptions` rewritten — it no longer promises unconditional headerless passthrough
21
+
22
+ ### Added — adapter type exports for wrappers
23
+
24
+ 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:
25
+
26
+ | Adapter | App (new) | Request (new) | Response (new) | `next` (new) |
27
+ | -------- | ----------- | ------------------------- | ----------------------------- | ---------------------------------- |
28
+ | express | `ExpressApp` | — (`ExpressRequest`) | — (`ExpressResponse`) | — (`ExpressNext`) |
29
+ | fastify | `FastifyApp` | — (`FastifyRequest`) | `FastifyResponse` | `FastifyNext` |
30
+ | hono | `HonoApp` | `HonoRequest` | `HonoResponse` | `HonoNext` |
31
+ | koa | `KoaApp` | `KoaRequest` | `KoaResponse` | — (`KoaNext`) |
32
+ | h3 | — (`H3App`) | `H3Request` | `H3Response` | `H3Next` |
33
+
34
+ - **`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
35
+ - **Hono had no request type.** Only `HonoContext` was exported, forcing consumers to cast the request object structurally
36
+ - **`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`
37
+ - **`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
38
+ - **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
39
+
40
+ ### Tests
41
+
42
+ - **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
43
+ - `isOriginRequestAllowed`: every row of the behaviour matrix verbatim, the sibling-subdomain ordering regression, the empty/whitespace-value normalisation, and case-sensitivity
44
+ - Per adapter: `Sec-Fetch-Site: cross-site` with no `Origin` → `403`, `same-origin` → `200`, and the sibling-subdomain regression (`allowlisted Origin` + `same-site` → `200`)
45
+ - 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
46
+ - **556 tests, 100% on all metrics**
47
+
48
+ ### Chores
49
+
50
+ - Bump version to `0.3.6` (`package.json`, `deno.json`)
51
+
3
52
  ## [0.3.5] - 2026-09-26
4
53
 
5
54
  ### Features
@@ -1,32 +1,44 @@
1
1
  import { Connect, ViteDevServer } from "vite";
2
- import { BodyResult, JsonValue, MiddlewareOptions, RpcPluginOptions } from "@thednp/rpc";
2
+ import { BodyResult, MiddlewareOptions, RpcPluginOptions } 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 RpcPluginOptions["adapter"] = "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.
@@ -108,7 +145,7 @@ export declare const isExpressRequest: (req: IncomingMessage | Request) => req i
108
145
  * @param res - A Node ServerResponse or Express Response
109
146
  * @returns True if the response is an Express Response
110
147
  */
111
- export declare const isExpressResponse: (res: ServerResponse | Response) => res is Response;
148
+ export declare const isExpressResponse: (res: ServerResponse | Response$1) => res is Response$1;
112
149
  /**
113
150
  * Issues an HTTP redirect on an Express or raw Node ServerResponse.
114
151
  * Uses Express's native `res.redirect(status, location)` when an Express
@@ -120,7 +157,7 @@ export declare const isExpressResponse: (res: ServerResponse | Response) => res
120
157
  * @param location - The URL to redirect to
121
158
  * @param status - HTTP status code, defaults to 303
122
159
  */
123
- export declare const redirect: (res: ServerResponse | Response, location: string, status?: number) => void;
160
+ export declare const redirect: (res: ServerResponse | Response$1, location: string, status?: number) => void;
124
161
  /**
125
162
  * Type guard that checks whether a request has a pre-parsed body (`body` property).
126
163
  * Used to detect if a body-parser middleware already consumed the stream.
@@ -141,7 +178,7 @@ export declare const getRequestDetails: (request: Request | IncomingMessage) =>
141
178
  * @param response - Express or Node.js server response object
142
179
  * @returns A ResponseDetails object with setHeader, setStatusCode, and sendResponse helpers
143
180
  */
144
- export declare const getResponseDetails: (response: Response | ServerResponse) => ResponseDetails;
181
+ export declare const getResponseDetails: (response: Response$1 | ServerResponse) => ResponseDetails;
145
182
  //#endregion
146
- export type { Express, ExpressMiddlewareFn, ExpressMiddlewareHooks, ExpressMiddlewareOptions, ExpressNext, ExpressRequest, ExpressResponse, RequestDetails, ResponseDetails };
183
+ export type { Express, ExpressApp, ExpressMiddlewareFn, ExpressMiddlewareHooks, ExpressMiddlewareOptions, ExpressNext, ExpressRequest, ExpressResponse, RequestDetails, ResponseDetails };
147
184
  //# 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":";;;;;;;;;;;;;;;;;KA0HY;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,aAAa,WAAW;;;;KAIxB,YAAY,gBAAgB,YAAY;;;;;;;;;;;KC1HxC;;EAEV;;EAEA,YAAY,cAAc;;EAE1B;;EAEA,gBAAgB;;EAEhB,eAAe,cAAc,QAAQ;;;;;;;KAQ3B;;EAEV;;EAEA;;EAEA,cAAc;;EAEd,SAAS;;EAET;;;;;;;KCxBU,2BAA2B;;;;;KAM3B,uBACV,UAAU,yCAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiBA,YACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;;;;;;qBCaM,kBAAkB;;;;;;;;;;qBAiFlB,qBAAqB;;;;;;;;wBCrHZ,UAAU,KAAKC,YAAO;;;;;;wBAc5B,WAAW,KAAKA,WAAS,MAAM;;;;;;;;qBAWlC,WAAQ,KACdC,UAAiB,oBACrB,QAAQ;;;;;;qBAqFE,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"}
@@ -1,4 +1,4 @@
1
- import { escapeRegExp, formatError, getGlobalPrefix, hasContentTypeMismatch, isOriginAllowed, provideRequestContext, scanForServerFiles } from "@thednp/rpc/server";
1
+ import { escapeRegExp, formatError, getGlobalPrefix, hasContentTypeMismatch, isOriginRequestAllowed, provideRequestContext, scanForServerFiles } from "@thednp/rpc/server";
2
2
  const defaultRPCOptions = {
3
3
  rpcPrefix: "__rpc",
4
4
  adapter: "express",
@@ -300,7 +300,7 @@ const createRPCMiddleware = (initialOptions = {}) => {
300
300
  const { url: path, searchParams } = getRequestDetails(req);
301
301
  const { sendResponse } = getResponseDetails(res);
302
302
  if (prefixRegex && !prefixRegex.test(path)) return;
303
- if (!isOriginAllowed(options.origin, req.headers.origin)) {
303
+ if (!isOriginRequestAllowed(options.origin, req.headers.origin, req.headers["sec-fetch-site"])) {
304
304
  sendResponse(403, { error: REQUEST_FORBIDDEN });
305
305
  return;
306
306
  }
@@ -1 +1 @@
1
- {"version":3,"file":"express.mjs","names":[],"sources":["../../src/options.ts","../../src/functionsMap.ts","../../src/constants.ts","../../src/server-helpers.ts","../../src/express/helpers.ts","../../src/express/createMiddleware.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","import type { ServerFnEntry } from \"./types.d.ts\";\nimport { defaultPrefix } from \"./options.ts\";\n\n/**\n * Global symbol under which the shared `serverFunctionsByPrefix` map is stored\n * on `globalThis`. Keeping it on a `Symbol.for` key makes it instance-stable\n * across the bundled entry copies (`index.mjs`, `server.mjs`, `express.mjs`,\n * ...) and dev-server hot reloads, exactly like the request-context storage in\n * `context.ts`. Without this, `scanForServerFiles` (bundled into the plugin)\n * would populate a map copy the adapter middleware could not read.\n */\nconst functionsMapSymbol = Symbol.for(\"thednp.rpc.functionsMap\");\n\n/**\n * Map of rpcPrefix -> Map of function names -> ServerFnEntry\n * Enables multiple RPC instances with different prefixes to coexist\n * without name collisions.\n */\nexport const serverFunctionsByPrefix: Map<\n string,\n Map<string, ServerFnEntry>\n> = ((globalThis as Record<symbol, Map<string, Map<string, ServerFnEntry>>>)[\n functionsMapSymbol\n] ??= new Map());\n\n/**\n * Gets or creates the function map for a specific prefix.\n * @param prefix - The RPC prefix (e.g., \"__rpc\", \"v1:rpc\", \"admin:rpc\")\n * @returns Map of function names to ServerFnEntry for that prefix\n */\nexport const getFunctionsForPrefix = (\n prefix: string,\n): Map<string, ServerFnEntry> => {\n if (!serverFunctionsByPrefix.has(prefix)) {\n serverFunctionsByPrefix.set(prefix, new Map());\n }\n return serverFunctionsByPrefix.get(prefix)!;\n};\n\n/**\n * Backward compatibility: default map for the default prefix.\n * Legacy code can still use serverFunctionsMap.set(name, entry).\n */\nexport const serverFunctionsMap: Map<string, ServerFnEntry> = {\n get: (key: string) => getFunctionsForPrefix(defaultPrefix).get(key),\n set: (key: string, value: ServerFnEntry) =>\n getFunctionsForPrefix(defaultPrefix).set(key, value),\n has: (key: string) => getFunctionsForPrefix(defaultPrefix).has(key),\n delete: (key: string) => getFunctionsForPrefix(defaultPrefix).delete(key),\n clear: () => getFunctionsForPrefix(defaultPrefix).clear(),\n get size() {\n return getFunctionsForPrefix(defaultPrefix).size;\n },\n entries: () => getFunctionsForPrefix(defaultPrefix).entries(),\n keys: () => getFunctionsForPrefix(defaultPrefix).keys(),\n values: () => getFunctionsForPrefix(defaultPrefix).values(),\n forEach: (\n callback: (\n value: ServerFnEntry,\n key: string,\n map: Map<string, ServerFnEntry>,\n ) => void,\n ) => getFunctionsForPrefix(defaultPrefix).forEach(callback),\n [Symbol.iterator]: () =>\n getFunctionsForPrefix(defaultPrefix)[Symbol.iterator](),\n} as unknown as Map<string, ServerFnEntry>;\n","export const OPERATION_ABORTED = \"Operation aborted\";\n\nexport const REQUEST_CANCELLED = \"Request was cancelled\";\n\nexport const FETCH_ERROR_PREFIX = \"Fetch error: \";\n\nexport const NO_SERVER_FUNCTION_FOUND = \"No server function found.\";\n\nexport const ERROR_LOADING_FILE = \"Error loading file:\";\n\nexport const FUNCTION_NOT_FOUND = \"Function not found\";\n\nexport const METHOD_NOT_ALLOWED = \"Method Not Allowed\";\n\nexport const REQUEST_FORBIDDEN = \"Forbidden\";\n\nexport const UNSUPPORTED_MEDIA_TYPE = \"Unsupported Media Type\";\n\nexport const BAD_REQUEST = \"Bad Request\";\n\nexport const INTERNAL_SERVER_ERROR = \"Internal Server Error\";\n\nexport const CLIENT_DISCONNECTED = \"client disconnected\";\n\n/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */\nexport const MIDDLEWARE_NAME_USED = (name: string) =>\n `The middleware name \"${name}\" is already used.`;\n\n/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */\nexport const INVALID_IDENTIFIER = (label: string, name: string) =>\n `Invalid ${label}: \"${name}\" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;\n\n/** Error message when a value fails the safe-path-segment validation. @param label - What kind of value was being validated. @param segment - The rejected value */\nexport const INVALID_PATH_SEGMENT = (label: string, segment: string) =>\n `Invalid ${label}: \"${segment}\" must match /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/`;\n\n/** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */\nexport const CONFIG_FILE_NOT_FOUND = (\n configFile: string,\n configFilePath: string,\n) =>\n ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;\n\nexport const NO_CONFIG_FOUND =\n ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n\n/** Error template for duplicate server function names across files. @param name - The duplicate registered name */\nexport const DUPLICATE_FUNCTION_NAME = (name: string) =>\n `Duplicate server function \"${name}\" detected. Each server function must have a unique name. Remove or rename the duplicate.`;\n","/** @module Server-side utilities. Exports the `RPCError` class for typed server-side errors, `formatError` for middleware error responses, `isFormContentType` and `hasContentTypeMismatch` for content-type validation, and `walkGlobFiles` for recursively discovering `*.server.*` files. Never import this module in client code — it is server-only. */\nimport type { ContentType, JsonObject, JsonValue } from \"./types.d.ts\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { INTERNAL_SERVER_ERROR } from \"./constants.ts\";\n\nconst GLOB_REGEX = /^.+\\.server\\.(ts|js|mjs|mts)$/;\n\n/**\n * Recursively walks `dir` and collects absolute paths to files whose\n * basename matches the `*.server.{ts,js,mjs,mts}` glob pattern.\n */\nexport const walkGlobFiles = async (dir: string): Promise<string[]> => {\n const results: string[] = [];\n const stack = [dir];\n while (stack.length) {\n const current = stack.pop()!;\n let entries;\n try {\n entries = await readdir(current, { withFileTypes: true });\n } catch (_e) {\n continue;\n }\n for (const entry of entries) {\n const fullPath = join(current, entry.name);\n if (entry.isFile() && GLOB_REGEX.test(entry.name)) {\n results.push(fullPath);\n } else if (entry.isDirectory()) {\n stack.push(fullPath);\n }\n }\n }\n return results;\n};\n\n/**\n * A typed error thrown from server functions.\n * The middleware serializes the `message` and `code` in the response,\n * allowing clients to recognise and handle specific error conditions.\n */\nexport class RPCError extends Error {\n /** Machine-readable error code (e.g. \"VALIDATION_FAILED\", \"UNAUTHORIZED\") */\n code: string;\n /** Optional diagnostic payload */\n data?: JsonValue;\n constructor(message: string, code = \"INTERNAL\", data?: JsonValue) {\n super(message);\n this.name = \"RPCError\";\n this.code = code;\n this.data = data;\n }\n}\n\n/**\n * Formats an error for the RPC middleware response.\n * In development the full `RPCError` payload is included so developers\n * can quickly identify issues. Unexpected exceptions never expose their\n * message — only the generic \"Internal Server Error\" is sent, preventing\n * information disclosure; server-side diagnostics are preserved via the\n * middleware's `console.error` logging.\n */\nexport const formatError = (\n err: unknown,\n isProduction: boolean,\n): JsonObject => {\n if (isProduction) {\n return { error: INTERNAL_SERVER_ERROR };\n }\n if (err instanceof RPCError) {\n const payload: JsonObject = {\n error: err.message || INTERNAL_SERVER_ERROR,\n code: err.code,\n };\n if (err.data !== undefined) payload.data = err.data;\n return payload;\n }\n return { error: INTERNAL_SERVER_ERROR };\n};\n\n/**\n * Checks whether a content type maps to a form encoding\n * (`multipart/form-data` or `application/x-www-form-urlencoded`).\n * Form-declared functions accept either encoding so native browser\n * submissions (urlencoded) keep working without JavaScript.\n */\nexport const isFormContentType = (contentType: string): boolean =>\n contentType === \"multipart/form-data\" ||\n contentType === \"application/x-www-form-urlencoded\";\n\n/**\n * Detects whether an incoming request's `Content-Type` header conflicts\n * with the function's declared content type. JSON and text functions are\n * enforced strictly (exact match wins), while form functions accept both\n * form encodings because the nojs fallback submits urlencoded forms to\n * multipart-declared endpoints. Requests without a `Content-Type` header\n * (curl, GET, legacy clients) are exempt from enforcement.\n * @param declared - The declared `contentType` from the server function options\n * @param rawHeader - The raw `Content-Type` request header, if present\n */\nexport const hasContentTypeMismatch = (\n declared: ContentType,\n rawHeader: string | undefined,\n): boolean => {\n // No Content-Type header → exempt (url bar, GET, curl compatibility)\n if (!rawHeader) return false;\n // Strip parameters (charset, boundary) before comparison\n const incomingType = rawHeader.trim().toLowerCase().split(\";\")[0].trim();\n if (isFormContentType(declared)) {\n // Forms: reject only non-form encodings (lenient between the two)\n return !isFormContentType(incomingType);\n }\n return incomingType !== declared;\n};\n\n/**\n * Escapes special regex metacharacters in a string.\n * Used to safely embed user-configurable values (like rpcPrefix) into regular expressions,\n * preventing ReDoS and regex injection attacks.\n * @param s - The raw string to escape\n * @returns The escaped string safe for use in new RegExp()\n */\nexport function escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Decides whether a request's `Origin` header is allowed by the configured\n * allowlist. Shared by all five adapters so the rule lives in exactly one place.\n *\n * - No `allowed` value (option unset) → everything passes: no validation.\n * - No `requestOrigin` header → passes, preserving curl/native-client access.\n * - Otherwise the header must match one of the entries exactly.\n *\n * A single string and a one-element array behave identically, so widening\n * `origin` to `string | string[]` is backward compatible.\n *\n * `Origin: null` (sandboxed iframes, `file://`, extension pages) is rejected\n * whenever an allowlist is set, because it never equals a real origin.\n * @param allowed - The configured `origin` option, if any\n * @param requestOrigin - The raw `Origin` request header, if present\n * @returns `true` when the request may proceed\n */\nexport const isOriginAllowed = (\n allowed: string | string[] | undefined,\n requestOrigin: string | undefined,\n): boolean => {\n if (!allowed || !requestOrigin) return true;\n return Array.isArray(allowed)\n ? allowed.includes(requestOrigin)\n : requestOrigin === allowed;\n};\n\nconst SAFE_URL_BASE = \"http://localhost\";\n\n/**\n * Parses a raw request URL against a fixed base without ever throwing.\n * Malformed request-targets (e.g. `/\\`, `//`, `/\\/`) make the WHATWG URL\n * parser throw `TypeError: Invalid URL`; the adapters call this while\n * building the per-request URL **before** their dispatch `try` block, so an\n * unhandled rejection there crashes raw `node:http` hosts (and Express 4).\n * On failure we fall back to the base root: the resulting pathname never\n * matches the RPC prefix, so the request is treated as non-RPC and falls\n * through to `next()` / 404 instead of crashing the process.\n * @param rawUrl - Raw request URL (path + optional query string)\n * @param base - Optional base URL, defaults to a fixed localhost origin\n * @returns A URL object; never throws\n */\nexport const safeURL = (rawUrl: string, base = SAFE_URL_BASE): URL => {\n try {\n return new URL(rawUrl, base);\n } catch {\n return new URL(\"/\", base);\n }\n};\n\nconst globalPrefixSymbol = Symbol.for(\"thednp.rpc.globalPrefix\");\n\n/** Global rpcPrefix from the last loaded config / middleware — fallback for functions without explicit prefix. */\nexport const getGlobalPrefix = (): string | undefined =>\n (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ];\n\nexport const setGlobalPrefix = (prefix: string | undefined): void => {\n if (prefix) {\n (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ] = prefix;\n } else {\n delete (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ];\n }\n};\n","// src/express/helpers.ts\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type {\n Request as ExpressRequest,\n Response as ExpressResponse,\n} from \"express\";\nimport type { BodyResult, JsonValue } from \"@thednp/rpc\";\nimport type { Buffer } from \"node:buffer\";\nimport type { ViteDevServer } from \"vite\";\nimport type { Express } from \"express\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\nimport type { RequestDetails, ResponseDetails } from \"./types.d.ts\";\nimport { safeURL } from \"../server-helpers.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to an Express app.\n * Dynamically imports loadRPCConfig and creates the middleware with loaded options.\n * @param app - Express application instance\n */\nexport async function attachRPC(app: Express) {\n // The main plugin entry statically imports Vite, so loadRPCConfig is\n // imported lazily: function bundles that never call attachRPC (e.g.\n // serverless functions) keep Vite out of the bundle (or externalized).\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n app.use(createRPCMiddleware(options));\n}\n\n/**\n * Attaches Vite's dev server middlewares to an Express app for development mode.\n * @param app - Express application instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: Express, vite: ViteDevServer) {\n app.use(vite.middlewares);\n}\n\n/**\n * Reads and parses the HTTP request body from an Express or Node IncomingMessage.\n * If a body parser middleware (e.g. express.json()) already consumed the stream,\n * uses the pre-parsed body from `req.body`.\n * @param req - Express or Node.js IncomingMessage\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n req: ExpressRequest | IncomingMessage,\n): Promise<BodyResult> => {\n return new Promise((resolve, reject) => {\n // If an Express body parser already consumed the stream\n // via app.use(express.json()), use req.body directly\n if (hasPreParsedBody(req) && req.body !== undefined) {\n // istanbul ignore next\n const contentType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart\n ? (req.body as Record<string, unknown>)\n : isJSON\n ? req.body\n : isUrlEncoded\n ? (req.body as Record<string, unknown>)\n : String(req.body),\n } as BodyResult);\n return;\n }\n\n // Else we parse the body right away\n let body = \"\";\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n req[method](\"data\", onData);\n req[method](\"end\", onEnd);\n req[method](\"error\", onError);\n };\n\n const onData = (chunk: Buffer) => {\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const incomingType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const isJSON = incomingType.includes(\"json\");\n const isMultipart = incomingType.includes(\"multipart/form-data\");\n const isUrlEncoded = incomingType.includes(\"urlencoded\");\n try {\n const data = isMultipart\n ? { raw: body }\n : isUrlEncoded\n ? Object.fromEntries(new URLSearchParams(body))\n : JSON.parse(body);\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart ? (data as Record<string, unknown>) : data,\n } as BodyResult);\n } catch (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n\n const onError = (err: Error) => {\n toggleListeners();\n\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n\n/**\n * Type guard that checks whether a request is an Express Request (has `originalUrl`).\n * @param req - A Node IncomingMessage or Express Request\n * @returns True if the request is an Express Request\n */\nexport const isExpressRequest = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"originalUrl\" in req;\n};\n\n/**\n * Type guard that checks whether a response is an Express Response (has `json` and `send` methods).\n * @param res - A Node ServerResponse or Express Response\n * @returns True if the response is an Express Response\n */\nexport const isExpressResponse = (\n res: ServerResponse | ExpressResponse,\n): res is ExpressResponse => {\n return \"json\" in res && \"send\" in res;\n};\n\n/**\n * Issues an HTTP redirect on an Express or raw Node ServerResponse.\n * Uses Express's native `res.redirect(status, location)` when an Express\n * Response is provided, otherwise writes the status code and `Location`\n * header directly on the raw `ServerResponse` (safe for Connect-compatible\n * middlewares and serverless adapters whose mock responses lack `.redirect`).\n * Defaults to `303 See Other` for convention (Post/Redirect/Get).\n * @param res - Express Response or raw Node ServerResponse\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n */\nexport const redirect = (\n res: ServerResponse | ExpressResponse,\n location: string,\n status = 303,\n): void => {\n if (isExpressResponse(res)) {\n res.redirect(status, location);\n return;\n }\n res.statusCode = status;\n res.setHeader(\"Location\", location);\n res.end();\n};\n\n/**\n * Type guard that checks whether a request has a pre-parsed body (`body` property).\n * Used to detect if a body-parser middleware already consumed the stream.\n * @param req - A Node IncomingMessage or Express Request\n * @returns True if the request has a body property\n */\nexport const hasPreParsedBody = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"body\" in req;\n};\n\n/**\n * Extracts normalized request details from an Express or Node IncomingMessage.\n * Parses the URL to extract pathname, search string, and search params.\n * @param request - Express or Node.js request object\n * @returns Normalized request details including URL, headers, and method\n */\nexport const getRequestDetails = (\n request: ExpressRequest | IncomingMessage,\n): RequestDetails => {\n const rawUrl = (\n isExpressRequest(request) ? request.originalUrl : request.url\n ) as string;\n const url = safeURL(rawUrl);\n\n return {\n url: url.pathname,\n search: url.search,\n searchParams: url.searchParams,\n headers: request.headers,\n method: request.method,\n };\n};\n\n/**\n * Wraps an Express or Node ServerResponse with a uniform API for setting headers,\n * status codes, and sending JSON responses. Handles the Express vs raw Node API differences.\n * @param response - Express or Node.js server response object\n * @returns A ResponseDetails object with setHeader, setStatusCode, and sendResponse helpers\n */\nexport const getResponseDetails = (\n response: ExpressResponse | ServerResponse,\n): ResponseDetails => {\n const isResponseSent = response.headersSent || response.writableEnded;\n\n const setHeader = (name: string, value: string) => {\n if (isExpressResponse(response)) {\n response.header(name, value);\n } else {\n response.setHeader(name, value);\n }\n };\n\n const setStatusCode = (code: number) => {\n if (isExpressResponse(response)) {\n response.status(code);\n } else {\n response.statusCode = code;\n }\n };\n\n const sendResponse = (code: number, output: JsonValue) => {\n setStatusCode(code);\n setHeader(\"Content-Type\", \"application/json\");\n\n if (isExpressResponse(response)) {\n response.send(JSON.stringify(output));\n } else {\n response.end(JSON.stringify(output));\n }\n };\n\n return {\n isResponseSent,\n setHeader,\n statusCode: response.statusCode,\n setStatusCode,\n sendResponse,\n };\n};\n","// src/express/createMidleware.ts\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type {\n NextFunction,\n Request as ExpressRequest,\n Response as ExpressResponse,\n} from \"express\";\nimport type {\n ExpressMiddlewareFn,\n ExpressMiddlewareOptions,\n} from \"./types.d.ts\";\nimport type { Connect } from \"vite\";\nimport type { JsonValue } from \"../types.d.ts\";\nimport type { RequestEvent } from \"@thednp/rpc/server\";\nimport {\n escapeRegExp,\n formatError,\n getGlobalPrefix,\n hasContentTypeMismatch,\n isOriginAllowed,\n provideRequestContext,\n scanForServerFiles,\n} from \"@thednp/rpc/server\";\nimport { getFunctionsForPrefix } from \"../functionsMap.ts\";\nimport {\n defaultMiddlewareOptions,\n defaultPrefix,\n defaultRPCOptions,\n} from \"../options.ts\";\nimport {\n getRequestDetails,\n getResponseDetails,\n readBody,\n redirect as expressRedirect,\n} from \"./helpers.ts\";\nimport {\n BAD_REQUEST,\n CLIENT_DISCONNECTED,\n FUNCTION_NOT_FOUND,\n METHOD_NOT_ALLOWED,\n MIDDLEWARE_NAME_USED,\n REQUEST_FORBIDDEN,\n UNSUPPORTED_MEDIA_TYPE,\n} from \"../constants.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates an Express middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated — reusing a name throws an error.\n * Prefix and path regexes are compiled once at creation time (hoisted) for performance.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns An Express middleware function\n */\nexport const createMiddleware: ExpressMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as ExpressMiddlewareOptions;\n const middlewareName = options.name;\n let rpcPrefix = options.rpcPrefix;\n const path = options.path;\n const handler = options.handler;\n\n let name = middlewareName;\n if (!name) {\n name = \"viteRPCMiddleware-\" + middlewareCount;\n middlewareCount += 1;\n }\n if (middlewareStack.has(name)) {\n throw new Error(MIDDLEWARE_NAME_USED(name));\n }\n middlewareStack.add(name);\n\n // Hoist regex compilation out of per-request path. Escape the prefix to\n // prevent regex injection via metacharacters in the config string.\n const prefixRegex: RegExp | null = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : null;\n const pathMatcher: RegExp | null = path\n ? (typeof path === \"string\" ? new RegExp(path) : path)\n : null;\n\n const middlewareHandler = async (\n req: IncomingMessage | ExpressRequest,\n res: ServerResponse | ExpressResponse,\n next: Connect.NextFunction | NextFunction,\n ) => {\n const { url } = getRequestDetails(req);\n\n // No need to continue when no handler provided\n if (!handler) {\n return next?.();\n }\n\n // Path matching\n if (pathMatcher && !pathMatcher.test(url)) return next?.();\n\n // rpcPrefix matching (boundary-safe via escaped regex)\n if (prefixRegex && !prefixRegex.test(url)) {\n return next?.();\n }\n\n rpcPrefix = (rpcPrefix ?? defaultPrefix) as string;\n\n // When serving from production server, scan for server files\n if (getFunctionsForPrefix(rpcPrefix).size === 0) {\n await scanForServerFiles({\n rpcPrefix,\n serverFiles: options.serverFiles,\n scanRoot: options.scanRoot,\n } as never);\n }\n\n // Execute handler\n await handler(req, res, next);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Express RPC middleware that routes incoming requests to registered server functions.\n * Reads the request body, dispatches to the matching function via getFunctionsForPrefix,\n * and sends the JSON-serialized result. Handles client disconnection via abort signals.\n * Supports multi-prefix setups where different middleware instances can route to functions\n * registered under different prefixes.\n * @param initialOptions - Options including rpcPrefix for URL routing and prefix-scoped function lookup\n * @returns An Express middleware function\n */\nexport const createRPCMiddleware: ExpressMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as ExpressMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const rpcPrefix = options.rpcPrefix;\n const prefix = rpcPrefix || getGlobalPrefix() || defaultPrefix;\n\n const prefixRegex = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${prefix}/`;\n\n return createMiddleware({\n ...options,\n handler: async (\n req: IncomingMessage | ExpressRequest,\n res: ServerResponse | ExpressResponse,\n _next: NextFunction | Connect.NextFunction,\n ) => {\n const { url: path, searchParams } = getRequestDetails(req);\n const { sendResponse } = getResponseDetails(res);\n\n // Validate the url starts with the prefix via the escaped regex\n // istanbul ignore next\n if (prefixRegex && !prefixRegex.test(path)) {\n // falls through to next handler (never reached in practice; the outer\n // createMiddleware already gates on this, but kept for defense-in-depth)\n return;\n }\n\n // Optional origin check: reject requests whose Origin header is not in\n // the configured allowlist. Requests without an Origin header\n // (curl, native clients) pass through unchecked.\n if (!isOriginAllowed(options.origin, req.headers.origin)) {\n sendResponse(403, { error: REQUEST_FORBIDDEN });\n return;\n }\n\n const functionName = path.replace(prefixReplace, \"\");\n // Look up function in the prefix-scoped map\n const serverFunctionsForPrefix = getFunctionsForPrefix(prefix);\n const serverFunction = serverFunctionsForPrefix.get(functionName);\n\n if (!serverFunction) {\n sendResponse(404, { error: FUNCTION_NOT_FOUND });\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (req.method?.toUpperCase() !== method) {\n sendResponse(405, { error: METHOD_NOT_ALLOWED });\n return;\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = searchParams.get(\"args\");\n if (raw) {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n sendResponse(400, { error: BAD_REQUEST });\n return;\n }\n args = parsed as JsonValue[];\n }\n } else {\n // Content-type enforcement: strict for json/text, lenient between forms.\n // Requests without a Content-Type header are exempt (curl/GET compat).\n // Checked BEFORE readBody so mismatched bodies are never buffered.\n if (\n hasContentTypeMismatch(\n serverFunction.options?.contentType ?? \"application/json\",\n req.headers[\"content-type\"],\n )\n ) {\n sendResponse(415, { error: UNSUPPORTED_MEDIA_TYPE });\n return;\n }\n const body = await readBody(req);\n args = Array.isArray(body.data)\n ? (body.data as JsonValue[])\n : [body.data as JsonValue];\n }\n // ─── Dispatch ────────────────────────────────────────────────────\n // Establish the per-request context around the *entire* dispatch so\n // server functions (and async continuations spawned by their work)\n // read `getRequestContext()` and can call the framework-level\n // `redirect(location)`. The adapter-specific redirect is bound into the\n // context here; `serverFunction.handler` must be invoked inside the\n // context callback so its async increments run with the context live.\n const requestEvent: RequestEvent = {\n request: req,\n response: res,\n nativeEvent: { req, res },\n locals: (res as ExpressResponse).locals ?? {},\n functionName,\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n expressRedirect(res, location, status);\n },\n send: (status, body, headers) => {\n requestEvent.sent = { status, body, headers };\n const details = getResponseDetails(res);\n if (headers) {\n for (const [name, value] of Object.entries(headers)) {\n details.setHeader(name, value);\n }\n }\n details.sendResponse(status, body);\n },\n };\n\n const { data, cancel } = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n req.on(\"close\", onClose);\n const result = await data;\n req.off(\"close\", onClose);\n\n // Skip the JSON send when the server function issued a redirect or\n // short-circuited with `send`; the bound adapter already wrote the\n // response. Express may also have ended the response via headersSent.\n // istanbul ignore else\n if (\n !requestEvent.redirected &&\n !requestEvent.sent &&\n !res.headersSent\n ) {\n sendResponse(200, { data: result });\n }\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n sendResponse(500, formatError(err, isProduction));\n }\n },\n });\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"express.mjs","names":[],"sources":["../../src/options.ts","../../src/functionsMap.ts","../../src/constants.ts","../../src/server-helpers.ts","../../src/express/helpers.ts","../../src/express/createMiddleware.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","import type { ServerFnEntry } from \"./types.d.ts\";\nimport { defaultPrefix } from \"./options.ts\";\n\n/**\n * Global symbol under which the shared `serverFunctionsByPrefix` map is stored\n * on `globalThis`. Keeping it on a `Symbol.for` key makes it instance-stable\n * across the bundled entry copies (`index.mjs`, `server.mjs`, `express.mjs`,\n * ...) and dev-server hot reloads, exactly like the request-context storage in\n * `context.ts`. Without this, `scanForServerFiles` (bundled into the plugin)\n * would populate a map copy the adapter middleware could not read.\n */\nconst functionsMapSymbol = Symbol.for(\"thednp.rpc.functionsMap\");\n\n/**\n * Map of rpcPrefix -> Map of function names -> ServerFnEntry\n * Enables multiple RPC instances with different prefixes to coexist\n * without name collisions.\n */\nexport const serverFunctionsByPrefix: Map<\n string,\n Map<string, ServerFnEntry>\n> = ((globalThis as Record<symbol, Map<string, Map<string, ServerFnEntry>>>)[\n functionsMapSymbol\n] ??= new Map());\n\n/**\n * Gets or creates the function map for a specific prefix.\n * @param prefix - The RPC prefix (e.g., \"__rpc\", \"v1:rpc\", \"admin:rpc\")\n * @returns Map of function names to ServerFnEntry for that prefix\n */\nexport const getFunctionsForPrefix = (\n prefix: string,\n): Map<string, ServerFnEntry> => {\n if (!serverFunctionsByPrefix.has(prefix)) {\n serverFunctionsByPrefix.set(prefix, new Map());\n }\n return serverFunctionsByPrefix.get(prefix)!;\n};\n\n/**\n * Backward compatibility: default map for the default prefix.\n * Legacy code can still use serverFunctionsMap.set(name, entry).\n */\nexport const serverFunctionsMap: Map<string, ServerFnEntry> = {\n get: (key: string) => getFunctionsForPrefix(defaultPrefix).get(key),\n set: (key: string, value: ServerFnEntry) =>\n getFunctionsForPrefix(defaultPrefix).set(key, value),\n has: (key: string) => getFunctionsForPrefix(defaultPrefix).has(key),\n delete: (key: string) => getFunctionsForPrefix(defaultPrefix).delete(key),\n clear: () => getFunctionsForPrefix(defaultPrefix).clear(),\n get size() {\n return getFunctionsForPrefix(defaultPrefix).size;\n },\n entries: () => getFunctionsForPrefix(defaultPrefix).entries(),\n keys: () => getFunctionsForPrefix(defaultPrefix).keys(),\n values: () => getFunctionsForPrefix(defaultPrefix).values(),\n forEach: (\n callback: (\n value: ServerFnEntry,\n key: string,\n map: Map<string, ServerFnEntry>,\n ) => void,\n ) => getFunctionsForPrefix(defaultPrefix).forEach(callback),\n [Symbol.iterator]: () =>\n getFunctionsForPrefix(defaultPrefix)[Symbol.iterator](),\n} as unknown as Map<string, ServerFnEntry>;\n","export const OPERATION_ABORTED = \"Operation aborted\";\n\nexport const REQUEST_CANCELLED = \"Request was cancelled\";\n\nexport const FETCH_ERROR_PREFIX = \"Fetch error: \";\n\nexport const NO_SERVER_FUNCTION_FOUND = \"No server function found.\";\n\nexport const ERROR_LOADING_FILE = \"Error loading file:\";\n\nexport const FUNCTION_NOT_FOUND = \"Function not found\";\n\nexport const METHOD_NOT_ALLOWED = \"Method Not Allowed\";\n\nexport const REQUEST_FORBIDDEN = \"Forbidden\";\n\nexport const UNSUPPORTED_MEDIA_TYPE = \"Unsupported Media Type\";\n\nexport const BAD_REQUEST = \"Bad Request\";\n\nexport const INTERNAL_SERVER_ERROR = \"Internal Server Error\";\n\nexport const CLIENT_DISCONNECTED = \"client disconnected\";\n\n/** Returns a warning when a middleware name is reused, preventing registration conflicts. @param name - The duplicate middleware name */\nexport const MIDDLEWARE_NAME_USED = (name: string) =>\n `The middleware name \"${name}\" is already used.`;\n\n/** Error message when a value fails the safe-identifier validation. @param label - What kind of value was being validated. @param name - The rejected value */\nexport const INVALID_IDENTIFIER = (label: string, name: string) =>\n `Invalid ${label}: \"${name}\" must match /^[A-Za-z_$][A-Za-z0-9_$]*$/`;\n\n/** Error message when a value fails the safe-path-segment validation. @param label - What kind of value was being validated. @param segment - The rejected value */\nexport const INVALID_PATH_SEGMENT = (label: string, segment: string) =>\n `Invalid ${label}: \"${segment}\" must match /^[A-Za-z0-9_$@:][A-Za-z0-9_$@:/-]*$/`;\n\n/** Warning message when a specified RPC config file cannot be resolved on disk. @param configFile - The requested config filename. @param configFilePath - The resolved absolute path */\nexport const CONFIG_FILE_NOT_FOUND = (\n configFile: string,\n configFilePath: string,\n) =>\n ` ⚠︎ The specified RPC config file ${configFile} cannot be found at ${configFilePath}, loading the defaults..`;\n\nexport const NO_CONFIG_FOUND =\n ` ⚡︎ No RPC config found, loading the defaults..`;\n\nexport const FAILED_LOAD_CONFIG = ` ⚠︎ Failed to load RPC config:`;\n\n/** Error template for duplicate server function names across files. @param name - The duplicate registered name */\nexport const DUPLICATE_FUNCTION_NAME = (name: string) =>\n `Duplicate server function \"${name}\" detected. Each server function must have a unique name. Remove or rename the duplicate.`;\n","/** @module Server-side utilities. Exports the `RPCError` class for typed server-side errors, `formatError` for middleware error responses, `isFormContentType` and `hasContentTypeMismatch` for content-type validation, and `walkGlobFiles` for recursively discovering `*.server.*` files. Never import this module in client code — it is server-only. */\nimport type { ContentType, JsonObject, JsonValue } from \"./types.d.ts\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { INTERNAL_SERVER_ERROR } from \"./constants.ts\";\n\nconst GLOB_REGEX = /^.+\\.server\\.(ts|js|mjs|mts)$/;\n\n/**\n * Recursively walks `dir` and collects absolute paths to files whose\n * basename matches the `*.server.{ts,js,mjs,mts}` glob pattern.\n */\nexport const walkGlobFiles = async (dir: string): Promise<string[]> => {\n const results: string[] = [];\n const stack = [dir];\n while (stack.length) {\n const current = stack.pop()!;\n let entries;\n try {\n entries = await readdir(current, { withFileTypes: true });\n } catch (_e) {\n continue;\n }\n for (const entry of entries) {\n const fullPath = join(current, entry.name);\n if (entry.isFile() && GLOB_REGEX.test(entry.name)) {\n results.push(fullPath);\n } else if (entry.isDirectory()) {\n stack.push(fullPath);\n }\n }\n }\n return results;\n};\n\n/**\n * A typed error thrown from server functions.\n * The middleware serializes the `message` and `code` in the response,\n * allowing clients to recognise and handle specific error conditions.\n */\nexport class RPCError extends Error {\n /** Machine-readable error code (e.g. \"VALIDATION_FAILED\", \"UNAUTHORIZED\") */\n code: string;\n /** Optional diagnostic payload */\n data?: JsonValue;\n constructor(message: string, code = \"INTERNAL\", data?: JsonValue) {\n super(message);\n this.name = \"RPCError\";\n this.code = code;\n this.data = data;\n }\n}\n\n/**\n * Formats an error for the RPC middleware response.\n * In development the full `RPCError` payload is included so developers\n * can quickly identify issues. Unexpected exceptions never expose their\n * message — only the generic \"Internal Server Error\" is sent, preventing\n * information disclosure; server-side diagnostics are preserved via the\n * middleware's `console.error` logging.\n */\nexport const formatError = (\n err: unknown,\n isProduction: boolean,\n): JsonObject => {\n if (isProduction) {\n return { error: INTERNAL_SERVER_ERROR };\n }\n if (err instanceof RPCError) {\n const payload: JsonObject = {\n error: err.message || INTERNAL_SERVER_ERROR,\n code: err.code,\n };\n if (err.data !== undefined) payload.data = err.data;\n return payload;\n }\n return { error: INTERNAL_SERVER_ERROR };\n};\n\n/**\n * Checks whether a content type maps to a form encoding\n * (`multipart/form-data` or `application/x-www-form-urlencoded`).\n * Form-declared functions accept either encoding so native browser\n * submissions (urlencoded) keep working without JavaScript.\n */\nexport const isFormContentType = (contentType: string): boolean =>\n contentType === \"multipart/form-data\" ||\n contentType === \"application/x-www-form-urlencoded\";\n\n/**\n * Detects whether an incoming request's `Content-Type` header conflicts\n * with the function's declared content type. JSON and text functions are\n * enforced strictly (exact match wins), while form functions accept both\n * form encodings because the nojs fallback submits urlencoded forms to\n * multipart-declared endpoints. Requests without a `Content-Type` header\n * (curl, GET, legacy clients) are exempt from enforcement.\n * @param declared - The declared `contentType` from the server function options\n * @param rawHeader - The raw `Content-Type` request header, if present\n */\nexport const hasContentTypeMismatch = (\n declared: ContentType,\n rawHeader: string | undefined,\n): boolean => {\n // No Content-Type header → exempt (url bar, GET, curl compatibility)\n if (!rawHeader) return false;\n // Strip parameters (charset, boundary) before comparison\n const incomingType = rawHeader.trim().toLowerCase().split(\";\")[0].trim();\n if (isFormContentType(declared)) {\n // Forms: reject only non-form encodings (lenient between the two)\n return !isFormContentType(incomingType);\n }\n return incomingType !== declared;\n};\n\n/**\n * Decides whether a request may proceed, given the configured origin allowlist\n * and the two headers a browser can be made to reveal.\n *\n * Three tiers, evaluated in order — the first tier with a signal decides:\n *\n * 1. `origin` option unset → everything passes. No validation is performed.\n * 2. `Origin` present → the allowlist decides, exactly as {@link isOriginAllowed}.\n * 3. `Origin` absent but `Sec-Fetch-Site` present → allow only `same-origin`\n * and `none`; anything else (including an unrecognised value) is rejected.\n * 4. Both absent → passes. This is the deliberate, documented curl/native hole.\n *\n * Tier 2 must short-circuit ahead of tier 3. `Sec-Fetch-Site` is a coarse\n * four-value enum that cannot name a host, so on its own it would reject a\n * legitimate request from an allowlisted sibling subdomain (`same-site`). The\n * allowlist exists precisely to admit that case, and it can only do so while\n * `Origin` survives. `Sec-Fetch-Site` earns a vote only once the precise signal\n * has been stripped away by something in the chain — at which point there is\n * nothing left to trust, so it fails closed.\n *\n * Browsers never strip `Origin` themselves, so tier 3 can only fire when a\n * proxy, sanitising middleware, or misconfigured CDN removed it. No legitimate\n * browser request can regress.\n *\n * An empty (or whitespace-only) header value counts as **absent**, not as an\n * unrecognised signal. No browser emits an empty `Sec-Fetch-Site`, and adapters\n * disagree on what their header accessor returns for a missing header (Node's\n * `req.headers` yields `undefined`, Hono's `c.req.header()` may yield `\"\"`).\n * Normalising here keeps all five adapters behaving identically instead of\n * inheriting whichever convention their framework happens to use.\n * @param allowed - The configured `origin` option, if any\n * @param origin - The raw `Origin` request header, if present\n * @param site - The raw `Sec-Fetch-Site` request header, if present\n * @returns `true` when the request may proceed\n */\nexport const isOriginRequestAllowed = (\n allowed: string | string[] | undefined,\n origin: string | undefined,\n site: string | undefined,\n): boolean => {\n if (!allowed) return true; // tier 1 — the check is opt-in\n if (origin?.trim()) return isOriginAllowed(allowed, origin); // tier 2 — precise\n if (!site?.trim()) return true; // tier 4 — curl / native client\n // tier 3 — precision lost, so fail closed\n return site === \"same-origin\" || site === \"none\";\n};\n\n/**\n * Escapes special regex metacharacters in a string.\n * Used to safely embed user-configurable values (like rpcPrefix) into regular expressions,\n * preventing ReDoS and regex injection attacks.\n * @param s - The raw string to escape\n * @returns The escaped string safe for use in new RegExp()\n */\nexport function escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Decides whether a request's `Origin` header is allowed by the configured\n * allowlist. Shared by all five adapters so the rule lives in exactly one place.\n *\n * - No `allowed` value (option unset) → everything passes: no validation.\n * - No `requestOrigin` header → passes, preserving curl/native-client access.\n * - Otherwise the header must match one of the entries exactly.\n *\n * A single string and a one-element array behave identically, so widening\n * `origin` to `string | string[]` is backward compatible.\n *\n * `Origin: null` (sandboxed iframes, `file://`, extension pages) is rejected\n * whenever an allowlist is set, because it never equals a real origin.\n * @param allowed - The configured `origin` option, if any\n * @param requestOrigin - The raw `Origin` request header, if present\n * @returns `true` when the request may proceed\n */\nexport const isOriginAllowed = (\n allowed: string | string[] | undefined,\n requestOrigin: string | undefined,\n): boolean => {\n if (!allowed || !requestOrigin) return true;\n return Array.isArray(allowed)\n ? allowed.includes(requestOrigin)\n : requestOrigin === allowed;\n};\n\nconst SAFE_URL_BASE = \"http://localhost\";\n\n/**\n * Parses a raw request URL against a fixed base without ever throwing.\n * Malformed request-targets (e.g. `/\\`, `//`, `/\\/`) make the WHATWG URL\n * parser throw `TypeError: Invalid URL`; the adapters call this while\n * building the per-request URL **before** their dispatch `try` block, so an\n * unhandled rejection there crashes raw `node:http` hosts (and Express 4).\n * On failure we fall back to the base root: the resulting pathname never\n * matches the RPC prefix, so the request is treated as non-RPC and falls\n * through to `next()` / 404 instead of crashing the process.\n * @param rawUrl - Raw request URL (path + optional query string)\n * @param base - Optional base URL, defaults to a fixed localhost origin\n * @returns A URL object; never throws\n */\nexport const safeURL = (rawUrl: string, base = SAFE_URL_BASE): URL => {\n try {\n return new URL(rawUrl, base);\n } catch {\n return new URL(\"/\", base);\n }\n};\n\nconst globalPrefixSymbol = Symbol.for(\"thednp.rpc.globalPrefix\");\n\n/** Global rpcPrefix from the last loaded config / middleware — fallback for functions without explicit prefix. */\nexport const getGlobalPrefix = (): string | undefined =>\n (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ];\n\nexport const setGlobalPrefix = (prefix: string | undefined): void => {\n if (prefix) {\n (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ] = prefix;\n } else {\n delete (globalThis as unknown as Record<symbol, string | undefined>)[\n globalPrefixSymbol\n ];\n }\n};\n","// src/express/helpers.ts\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type {\n Request as ExpressRequest,\n Response as ExpressResponse,\n} from \"express\";\nimport type { BodyResult, JsonValue } from \"@thednp/rpc\";\nimport type { Buffer } from \"node:buffer\";\nimport type { ViteDevServer } from \"vite\";\nimport type { Express } from \"express\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\nimport type { RequestDetails, ResponseDetails } from \"./types.d.ts\";\nimport { safeURL } from \"../server-helpers.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to an Express app.\n * Dynamically imports loadRPCConfig and creates the middleware with loaded options.\n * @param app - Express application instance\n */\nexport async function attachRPC(app: Express) {\n // The main plugin entry statically imports Vite, so loadRPCConfig is\n // imported lazily: function bundles that never call attachRPC (e.g.\n // serverless functions) keep Vite out of the bundle (or externalized).\n const { loadRPCConfig } = await import(\"@thednp/rpc\");\n const { adapter: _adapter, ...options } = await loadRPCConfig();\n app.use(createRPCMiddleware(options));\n}\n\n/**\n * Attaches Vite's dev server middlewares to an Express app for development mode.\n * @param app - Express application instance\n * @param vite - Running Vite dev server\n */\nexport function attachVite(app: Express, vite: ViteDevServer) {\n app.use(vite.middlewares);\n}\n\n/**\n * Reads and parses the HTTP request body from an Express or Node IncomingMessage.\n * If a body parser middleware (e.g. express.json()) already consumed the stream,\n * uses the pre-parsed body from `req.body`.\n * @param req - Express or Node.js IncomingMessage\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = (\n req: ExpressRequest | IncomingMessage,\n): Promise<BodyResult> => {\n return new Promise((resolve, reject) => {\n // If an Express body parser already consumed the stream\n // via app.use(express.json()), use req.body directly\n if (hasPreParsedBody(req) && req.body !== undefined) {\n // istanbul ignore next\n const contentType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart\n ? (req.body as Record<string, unknown>)\n : isJSON\n ? req.body\n : isUrlEncoded\n ? (req.body as Record<string, unknown>)\n : String(req.body),\n } as BodyResult);\n return;\n }\n\n // Else we parse the body right away\n let body = \"\";\n\n const toggleListeners = (add?: boolean) => {\n const method = add ? \"on\" : \"off\";\n req[method](\"data\", onData);\n req[method](\"end\", onEnd);\n req[method](\"error\", onError);\n };\n\n const onData = (chunk: Buffer) => {\n body += chunk.toString();\n };\n\n const onEnd = () => {\n toggleListeners();\n const incomingType = req.headers[\"content-type\"]?.toLowerCase() || \"\";\n const isJSON = incomingType.includes(\"json\");\n const isMultipart = incomingType.includes(\"multipart/form-data\");\n const isUrlEncoded = incomingType.includes(\"urlencoded\");\n try {\n const data = isMultipart\n ? { raw: body }\n : isUrlEncoded\n ? Object.fromEntries(new URLSearchParams(body))\n : JSON.parse(body);\n resolve({\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isJSON\n ? \"application/json\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart ? (data as Record<string, unknown>) : data,\n } as BodyResult);\n } catch (_e) {\n resolve({ contentType: \"text/plain\", data: String(body) });\n }\n };\n\n const onError = (err: Error) => {\n toggleListeners();\n\n reject(err);\n };\n\n toggleListeners(true);\n });\n};\n\n/**\n * Type guard that checks whether a request is an Express Request (has `originalUrl`).\n * @param req - A Node IncomingMessage or Express Request\n * @returns True if the request is an Express Request\n */\nexport const isExpressRequest = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"originalUrl\" in req;\n};\n\n/**\n * Type guard that checks whether a response is an Express Response (has `json` and `send` methods).\n * @param res - A Node ServerResponse or Express Response\n * @returns True if the response is an Express Response\n */\nexport const isExpressResponse = (\n res: ServerResponse | ExpressResponse,\n): res is ExpressResponse => {\n return \"json\" in res && \"send\" in res;\n};\n\n/**\n * Issues an HTTP redirect on an Express or raw Node ServerResponse.\n * Uses Express's native `res.redirect(status, location)` when an Express\n * Response is provided, otherwise writes the status code and `Location`\n * header directly on the raw `ServerResponse` (safe for Connect-compatible\n * middlewares and serverless adapters whose mock responses lack `.redirect`).\n * Defaults to `303 See Other` for convention (Post/Redirect/Get).\n * @param res - Express Response or raw Node ServerResponse\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n */\nexport const redirect = (\n res: ServerResponse | ExpressResponse,\n location: string,\n status = 303,\n): void => {\n if (isExpressResponse(res)) {\n res.redirect(status, location);\n return;\n }\n res.statusCode = status;\n res.setHeader(\"Location\", location);\n res.end();\n};\n\n/**\n * Type guard that checks whether a request has a pre-parsed body (`body` property).\n * Used to detect if a body-parser middleware already consumed the stream.\n * @param req - A Node IncomingMessage or Express Request\n * @returns True if the request has a body property\n */\nexport const hasPreParsedBody = (\n req: IncomingMessage | ExpressRequest,\n): req is ExpressRequest => {\n return \"body\" in req;\n};\n\n/**\n * Extracts normalized request details from an Express or Node IncomingMessage.\n * Parses the URL to extract pathname, search string, and search params.\n * @param request - Express or Node.js request object\n * @returns Normalized request details including URL, headers, and method\n */\nexport const getRequestDetails = (\n request: ExpressRequest | IncomingMessage,\n): RequestDetails => {\n const rawUrl = (\n isExpressRequest(request) ? request.originalUrl : request.url\n ) as string;\n const url = safeURL(rawUrl);\n\n return {\n url: url.pathname,\n search: url.search,\n searchParams: url.searchParams,\n headers: request.headers,\n method: request.method,\n };\n};\n\n/**\n * Wraps an Express or Node ServerResponse with a uniform API for setting headers,\n * status codes, and sending JSON responses. Handles the Express vs raw Node API differences.\n * @param response - Express or Node.js server response object\n * @returns A ResponseDetails object with setHeader, setStatusCode, and sendResponse helpers\n */\nexport const getResponseDetails = (\n response: ExpressResponse | ServerResponse,\n): ResponseDetails => {\n const isResponseSent = response.headersSent || response.writableEnded;\n\n const setHeader = (name: string, value: string) => {\n if (isExpressResponse(response)) {\n response.header(name, value);\n } else {\n response.setHeader(name, value);\n }\n };\n\n const setStatusCode = (code: number) => {\n if (isExpressResponse(response)) {\n response.status(code);\n } else {\n response.statusCode = code;\n }\n };\n\n const sendResponse = (code: number, output: JsonValue) => {\n setStatusCode(code);\n setHeader(\"Content-Type\", \"application/json\");\n\n if (isExpressResponse(response)) {\n response.send(JSON.stringify(output));\n } else {\n response.end(JSON.stringify(output));\n }\n };\n\n return {\n isResponseSent,\n setHeader,\n statusCode: response.statusCode,\n setStatusCode,\n sendResponse,\n };\n};\n","// src/express/createMidleware.ts\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type {\n NextFunction,\n Request as ExpressRequest,\n Response as ExpressResponse,\n} from \"express\";\nimport type {\n ExpressMiddlewareFn,\n ExpressMiddlewareOptions,\n} from \"./types.d.ts\";\nimport type { Connect } from \"vite\";\nimport type { JsonValue } from \"../types.d.ts\";\nimport type { RequestEvent } from \"@thednp/rpc/server\";\nimport {\n escapeRegExp,\n formatError,\n getGlobalPrefix,\n hasContentTypeMismatch,\n isOriginRequestAllowed,\n provideRequestContext,\n scanForServerFiles,\n} from \"@thednp/rpc/server\";\nimport { getFunctionsForPrefix } from \"../functionsMap.ts\";\nimport {\n defaultMiddlewareOptions,\n defaultPrefix,\n defaultRPCOptions,\n} from \"../options.ts\";\nimport {\n getRequestDetails,\n getResponseDetails,\n readBody,\n redirect as expressRedirect,\n} from \"./helpers.ts\";\nimport {\n BAD_REQUEST,\n CLIENT_DISCONNECTED,\n FUNCTION_NOT_FOUND,\n METHOD_NOT_ALLOWED,\n MIDDLEWARE_NAME_USED,\n REQUEST_FORBIDDEN,\n UNSUPPORTED_MEDIA_TYPE,\n} from \"../constants.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates an Express middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated — reusing a name throws an error.\n * Prefix and path regexes are compiled once at creation time (hoisted) for performance.\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns An Express middleware function\n */\nexport const createMiddleware: ExpressMiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as ExpressMiddlewareOptions;\n const middlewareName = options.name;\n let rpcPrefix = options.rpcPrefix;\n const path = options.path;\n const handler = options.handler;\n\n let name = middlewareName;\n if (!name) {\n name = \"viteRPCMiddleware-\" + middlewareCount;\n middlewareCount += 1;\n }\n if (middlewareStack.has(name)) {\n throw new Error(MIDDLEWARE_NAME_USED(name));\n }\n middlewareStack.add(name);\n\n // Hoist regex compilation out of per-request path. Escape the prefix to\n // prevent regex injection via metacharacters in the config string.\n const prefixRegex: RegExp | null = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : null;\n const pathMatcher: RegExp | null = path\n ? (typeof path === \"string\" ? new RegExp(path) : path)\n : null;\n\n const middlewareHandler = async (\n req: IncomingMessage | ExpressRequest,\n res: ServerResponse | ExpressResponse,\n next: Connect.NextFunction | NextFunction,\n ) => {\n const { url } = getRequestDetails(req);\n\n // No need to continue when no handler provided\n if (!handler) {\n return next?.();\n }\n\n // Path matching\n if (pathMatcher && !pathMatcher.test(url)) return next?.();\n\n // rpcPrefix matching (boundary-safe via escaped regex)\n if (prefixRegex && !prefixRegex.test(url)) {\n return next?.();\n }\n\n rpcPrefix = (rpcPrefix ?? defaultPrefix) as string;\n\n // When serving from production server, scan for server files\n if (getFunctionsForPrefix(rpcPrefix).size === 0) {\n await scanForServerFiles({\n rpcPrefix,\n serverFiles: options.serverFiles,\n scanRoot: options.scanRoot,\n } as never);\n }\n\n // Execute handler\n await handler(req, res, next);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the Express RPC middleware that routes incoming requests to registered server functions.\n * Reads the request body, dispatches to the matching function via getFunctionsForPrefix,\n * and sends the JSON-serialized result. Handles client disconnection via abort signals.\n * Supports multi-prefix setups where different middleware instances can route to functions\n * registered under different prefixes.\n * @param initialOptions - Options including rpcPrefix for URL routing and prefix-scoped function lookup\n * @returns An Express middleware function\n */\nexport const createRPCMiddleware: ExpressMiddlewareFn = (\n initialOptions = {},\n) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as ExpressMiddlewareOptions;\n\n // Hoist prefix regex (escaped) and the literal prefix-for-replace out of the\n // per-request handler to avoid regex injection and per-request compilation.\n const rpcPrefix = options.rpcPrefix;\n const prefix = rpcPrefix || getGlobalPrefix() || defaultPrefix;\n\n const prefixRegex = rpcPrefix\n ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`)\n : /* istanbul ignore next */ null;\n const prefixReplace = `/${prefix}/`;\n\n return createMiddleware({\n ...options,\n handler: async (\n req: IncomingMessage | ExpressRequest,\n res: ServerResponse | ExpressResponse,\n _next: NextFunction | Connect.NextFunction,\n ) => {\n const { url: path, searchParams } = getRequestDetails(req);\n const { sendResponse } = getResponseDetails(res);\n\n // Validate the url starts with the prefix via the escaped regex\n // istanbul ignore next\n if (prefixRegex && !prefixRegex.test(path)) {\n // falls through to next handler (never reached in practice; the outer\n // createMiddleware already gates on this, but kept for defense-in-depth)\n return;\n }\n\n // Optional origin check. When Origin survives, the allowlist decides;\n // when it has been stripped, Sec-Fetch-Site is consulted instead and\n // fails closed. See `isOriginRequestAllowed` for the four tiers.\n if (\n !isOriginRequestAllowed(\n options.origin,\n req.headers.origin,\n req.headers[\"sec-fetch-site\"],\n )\n ) {\n sendResponse(403, { error: REQUEST_FORBIDDEN });\n return;\n }\n\n const functionName = path.replace(prefixReplace, \"\");\n // Look up function in the prefix-scoped map\n const serverFunctionsForPrefix = getFunctionsForPrefix(prefix);\n const serverFunction = serverFunctionsForPrefix.get(functionName);\n\n if (!serverFunction) {\n sendResponse(404, { error: FUNCTION_NOT_FOUND });\n return;\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (req.method?.toUpperCase() !== method) {\n sendResponse(405, { error: METHOD_NOT_ALLOWED });\n return;\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = searchParams.get(\"args\");\n if (raw) {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n sendResponse(400, { error: BAD_REQUEST });\n return;\n }\n args = parsed as JsonValue[];\n }\n } else {\n // Content-type enforcement: strict for json/text, lenient between forms.\n // Requests without a Content-Type header are exempt (curl/GET compat).\n // Checked BEFORE readBody so mismatched bodies are never buffered.\n if (\n hasContentTypeMismatch(\n serverFunction.options?.contentType ?? \"application/json\",\n req.headers[\"content-type\"],\n )\n ) {\n sendResponse(415, { error: UNSUPPORTED_MEDIA_TYPE });\n return;\n }\n const body = await readBody(req);\n args = Array.isArray(body.data)\n ? (body.data as JsonValue[])\n : [body.data as JsonValue];\n }\n // ─── Dispatch ────────────────────────────────────────────────────\n // Establish the per-request context around the *entire* dispatch so\n // server functions (and async continuations spawned by their work)\n // read `getRequestContext()` and can call the framework-level\n // `redirect(location)`. The adapter-specific redirect is bound into the\n // context here; `serverFunction.handler` must be invoked inside the\n // context callback so its async increments run with the context live.\n const requestEvent: RequestEvent = {\n request: req,\n response: res,\n nativeEvent: { req, res },\n locals: (res as ExpressResponse).locals ?? {},\n functionName,\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n expressRedirect(res, location, status);\n },\n send: (status, body, headers) => {\n requestEvent.sent = { status, body, headers };\n const details = getResponseDetails(res);\n if (headers) {\n for (const [name, value] of Object.entries(headers)) {\n details.setHeader(name, value);\n }\n }\n details.sendResponse(status, body);\n },\n };\n\n const { data, cancel } = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onClose = () => cancel(CLIENT_DISCONNECTED);\n req.on(\"close\", onClose);\n const result = await data;\n req.off(\"close\", onClose);\n\n // Skip the JSON send when the server function issued a redirect or\n // short-circuited with `send`; the bound adapter already wrote the\n // response. Express may also have ended the response via headersSent.\n // istanbul ignore else\n if (\n !requestEvent.redirected &&\n !requestEvent.sent &&\n !res.headersSent\n ) {\n sendResponse(200, { data: result });\n }\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n sendResponse(500, formatError(err, isProduction));\n }\n },\n });\n};\n"],"mappings":"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"}