@thednp/rpc 0.3.3 → 0.3.4
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 +8 -4
- package/CHANGELOG.md +25 -1
- package/dist/express/express.d.mts.map +1 -1
- package/dist/fastify/fastify.d.mts.map +1 -1
- package/dist/fastify/plugin/fastify/plugin.d.mts.map +1 -1
- package/dist/h3/h3.d.mts.map +1 -1
- package/dist/h3/h3.mjs +1 -1
- package/dist/h3/h3.mjs.map +1 -1
- package/dist/helpers/helpers.d.mts +11 -2
- package/dist/helpers/helpers.d.mts.map +1 -1
- package/dist/helpers/helpers.mjs +16 -3
- package/dist/helpers/helpers.mjs.map +1 -1
- package/dist/hono/hono.d.mts.map +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/server/server.d.mts.map +1 -1
- package/llms.txt +2 -2
- package/package.json +10 -10
package/AGENTS.md
CHANGED
|
@@ -126,12 +126,16 @@ The tsdown.config.ts produces multiple entries:
|
|
|
126
126
|
- **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
127
|
- **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
128
|
- **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**:
|
|
129
|
+
- **Koa URL normalization** → **URL normalization (all adapters)**: every adapter parses the request URL through the shared `safeURL()` helper (`src/server-helpers.ts`) to strip query strings and normalize encoding before prefix checking. `safeURL` **never throws** — malformed request-targets (`/\`, `//`, `/\/`) make the WHATWG parser raise `TypeError: Invalid URL`, and the adapters parse the URL *before* their dispatch `try` block, so an unguarded throw became an unhandled rejection that crashed raw `node:http` hosts and Express 4. On failure it falls back to the base root, so the pathname never matches the prefix and the request degrades to `next()`/404
|
|
130
|
+
- **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
|
|
130
131
|
- **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.
|
|
131
|
-
- **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.
|
|
132
|
-
- **
|
|
132
|
+
- **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
|
|
133
|
+
- **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
|
+
- **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
|
+
- **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
|
|
133
136
|
- **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.
|
|
134
137
|
- **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
|
|
138
|
+
- **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
|
|
135
139
|
|
|
136
140
|
## Threat Model
|
|
137
141
|
|
|
@@ -141,7 +145,7 @@ The framework's security boundary is the **RPC prefix-gated HTTP endpoint**. Inp
|
|
|
141
145
|
| -----------------------| -------------------------------| ---------------------| --------------------------------------------------------------------------|
|
|
142
146
|
| `rpcPrefix` (config) | `rpc.config.ts` / dev options | Developer-trusted | Escaped before regex; validated before code gen |
|
|
143
147
|
| Function export name | `src/api/server.ts` exports | Developer-trusted | Validated against identifier regex before client codegen |
|
|
144
|
-
| HTTP request URL | Untrusted client | Boundary-filtered | Prefix regex (escaped, anchored, hoisted);
|
|
148
|
+
| HTTP request URL | Untrusted client | Boundary-filtered | Prefix regex (escaped, anchored, hoisted); non-throwing `safeURL()` normalization |
|
|
145
149
|
| HTTP request body | Untrusted client | Capped by framework | Framework body parsers cap JSON and raw bodies |
|
|
146
150
|
|
|
147
151
|
**Attackers cannot**:
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.4] - 2026-09-26
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`unwrapEnvelope` now honors the error contract it was documented as having** (`src/client-helpers.ts`, exported from `@thednp/rpc/helpers`): the helper added in 0.3.3 was a pure property accessor — it returned `json.data` when present and otherwise passed the input through **unchanged**, with no `error` check and no status-code awareness. Given a 403 or 500 body it returned the error object as though it were a result, so a native client that checked the unwrapped value but not `res.ok` treated an authorization failure as a success — a fail-open error swallow in a helper whose entire purpose is native-client ergonomics. It now throws on a **top-level** `error` key (which the server emits only for `400`/`404`/`405`/`415`/`500`), matching the sibling `handleResponse` in the same module, which always checked `result.error`. Discriminating on *error present **and** data absent* preserves the validation-as-data contract: `{ data: { error } }` is a `200` carrying a validation outcome and still resolves normally. Falsy-value unwrapping is preserved because the test is `"data" in envelope`, not truthiness. It remains status-code agnostic, so the `res.ok` check is still required and is now documented
|
|
8
|
+
- **h3 adapter now honors the global prefix fallback** (`src/h3/createMiddleware.ts:128`): `createRPCMiddleware` resolved its dispatch prefix as `rpcPrefix || defaultPrefix` while the other four adapters — and h3's own outer gate at line 88 — use `rpcPrefix || getGlobalPrefix() || defaultPrefix`. With a global prefix set and no explicit middleware option, h3 matched the prefix in the outer gate but then looked the function up in the `__rpc` map and returned 404. Fail-closed (a wrong prefix yields 404, never a cross-prefix dispatch), so this was an availability bug rather than an authorization bypass — but a real trap for multi-prefix setups on h3
|
|
9
|
+
- **SPA example enforces its body cap while streaming** (`examples/spa/body-limit.ts`): the middleware called `readBody` — fully buffering the request — and only then measured the result, so its 1 MB limit provided no protection against memory exhaustion, despite `wiki/security.md` prescribing enforcement *while streaming*. An unauthenticated client could stream an arbitrarily large body and the server would accumulate all of it before rejecting. The rewrite measures each chunk as it arrives, retains nothing past the cap, and drains-and-discards the remainder rather than closing on the spot — with a drain ceiling so the discard cannot become an unbounded slowloris. **The guarantee is a bounded memory footprint**: 5 MB, 20 MB, 50 MB and 60 MB uploads all peak at baseline heap (~9 MB) instead of buffering their payload. A clean `413` is delivered for moderately oversized requests by draining first; a client still streaming a very large body may see a connection reset instead, which is normal HTTP behavior — the primary win is that the payload is never resident
|
|
10
|
+
|
|
11
|
+
### Docs
|
|
12
|
+
|
|
13
|
+
- **`unwrapEnvelope` documentation corrected** (`CHANGELOG.md`, `llms.txt`, `wiki/client-usage.md`, `wiki/security.md`, `wiki/wire-protocol.md`): the 0.3.3 docs claimed the helper "re-throws `RPCError` on `{ error }` bodies" and shipped an example importing `RPCError` from `@thednp/rpc/helpers`, where it does not exist — that entry ships only `getClientStub`, `handleResponse`, `innerModule`, and `unwrapEnvelope`. The documented snippet degraded to `err instanceof undefined`, i.e. `TypeError: Right-hand side of 'instanceof' is not callable`, throwing a confusing `TypeError` precisely when handling a failure. `RPCError` is a **server-side** export (`@thednp/rpc/server`) and its `code`/`data` are stripped from production responses regardless; all docs now say so
|
|
14
|
+
- `wiki/security.md` — Koa-only "URL Normalization" section generalized to all adapters and the shared `safeURL()` helper, including why it must never throw; new `?args=` Must Be a JSON Array section; new "Native Clients: `unwrapEnvelope`" contract section; Origin Validation gains a "Residual Gaps" subsection covering the unconsulted `Sec-Fetch-Site` header (with a drop-in middleware snippet) and why top-level `GET` navigations bypass the check entirely; Content-Type Enforcement now states explicitly that a JSON-declared function does **not** accept form bodies — the leniency is one-directional, and the previous wording overstated it in both directions
|
|
15
|
+
- Fixed a broken anchor link to the `unwrapEnvelope` section in `wiki/wire-protocol.md`
|
|
16
|
+
- `AGENTS.md` — security posture section updated: `safeURL` normalization (all adapters), `?args=` array validation, one-directional content-type strictness, the shared client error contract, the streaming body-limit requirement, and a prefix-parity note to keep the five adapters in sync; threat-model table row corrected
|
|
17
|
+
- `llms.txt` — `unwrapEnvelope` contract, `400` added to the status-code list, content-type leniency direction
|
|
18
|
+
|
|
19
|
+
### Tests
|
|
20
|
+
|
|
21
|
+
- `unwrapEnvelope` gains coverage for the error contract: top-level `error` throws, non-string `error` stringifies, `{ data: { error } }` resolves (the validation-as-data regression guard), falsy `data` values (`false`/`0`/`""`) unwrap, and non-envelope objects pass through — **447 tests, 100% on all metrics**
|
|
22
|
+
|
|
23
|
+
### Chores
|
|
24
|
+
|
|
25
|
+
- Bump version to `0.3.4` (`package.json`, `deno.json`)
|
|
26
|
+
|
|
3
27
|
## [0.3.3] - 2026-09-15
|
|
4
28
|
|
|
5
29
|
### Features
|
|
@@ -7,7 +31,7 @@
|
|
|
7
31
|
- **Relaxed `JsonValue` constraint on `createServerFunction`** (`src/types.d.ts`, `src/createFunction.ts`): the generic `TResult` parameter of `ClientFunction`, `ServerFunction`, `ServerFunctionInit`, and `createServerFunction` no longer requires `extends JsonValue`. Only `getClientStub` and the internal `innerModule` retain the constraint for wire protocol safety. This eliminates double-casts and `LooseClientFunction` workarounds in wrapper libraries that define server functions with non-JSON return types
|
|
8
32
|
- **Hono header fallback in `getRequestMeta`** (`src/context.ts:218`): `getRequestMeta` now reads `req?.raw?.headers` as a fallback when `req?.headers` is undefined, matching Hono's internal request structure where native `Headers` live on `req.raw`. All other adapters pass headers via `req.headers` and are unaffected
|
|
9
33
|
- **`viteMiddleware` for Fastify** (`src/fastify/helpers.ts:40-68`): new `viteMiddleware(vite)` export returning an `onRequest` hook handler with `reply.hijack()` for streaming. Consistent with h3 and Hono's `viteMiddleware` — all three adapters now export a standalone middleware factory for custom Vite setups
|
|
10
|
-
- **`unwrapEnvelope<T>(json)` client helper** (`src/client-helpers.ts:44-55`, exported from `@thednp/rpc/helpers`): typed helper to unwrap `{ data }` wire protocol responses. For native HTTP clients or non-Vite toolchains that don't use the auto-generated fetch stubs.
|
|
34
|
+
- **`unwrapEnvelope<T>(json)` client helper** (`src/client-helpers.ts:44-55`, exported from `@thednp/rpc/helpers`): typed helper to unwrap `{ data }` wire protocol responses. For native HTTP clients or non-Vite toolchains that don't use the auto-generated fetch stubs. Added as a pure envelope accessor with no error handling — the fail-open error swallow and the accompanying documentation error were corrected in [0.3.4](#034---2026-09-26)
|
|
11
35
|
- **`silent` option on `loadRPCConfig` / `rpcPlugin`** (`src/config.ts:45`, `src/types.d.ts:239`, `src/index.ts:166`): new `silent?: boolean` suppresses the `NO_CONFIG_FOUND` warning. The plugin passes `devOptions.silent` through to `loadRPCConfig`, allowing wrapper plugins to define server functions directly without a config file
|
|
12
36
|
|
|
13
37
|
### Chores
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.d.mts","names":[],"sources":["../../src/express/types.d.ts","../../src/express/createMiddleware.ts","../../src/express/helpers.ts"],"mappings":";;;;;;;;KAgBY,2BAA2B;;;;;KAM3B,uBACV,UAAU,yCAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiB,UACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;KAOK;;EAEV;;EAEA,YAAY,cAAc;;EAE1B;;EAEA,gBAAgB;;EAEhB,eAAe,cAAc,QAAQ;;;;;KAM3B;;EAEV;;EAEA;;EAEA,cAAc;;EAEd,SAAS;;EAET;;;;;;;;;;;qBCrBW,kBAAkB;;;;;;;;;;qBAiFlB,qBAAqB;;;;;;;;wBCpHZ,UAAU,
|
|
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;;;;;;;;;;;qBCrBW,kBAAkB;;;;;;;;;;qBAiFlB,qBAAqB;;;;;;;;wBCpHZ,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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fastify.d.mts","names":[],"sources":["../../src/fastify/types.d.ts","../../src/fastify/createMiddleware.ts","../../src/types.d.ts","../../src/fastify/helpers.ts"],"mappings":";;;;;;;;;;;;;;;KAYY,oBACV,SAAS,iBACT,gBAAgB,QAAQ,+BACxB;;;;KAMU,uBAAuB;;;;KAKvB,6BAA6B,WAAW;;;;KAKxC,0BAA0B;;EAEpC;;;;;KAMU,2BAA2B;;;;;KAM3B,uBACV,UAAU,yCAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,
|
|
1
|
+
{"version":3,"file":"fastify.d.mts","names":["FastifyRequest","FastifyReply","FastifyRequest","FastifyReply"],"sources":["../../src/fastify/types.d.ts","../../src/fastify/createMiddleware.ts","../../src/types.d.ts","../../src/fastify/helpers.ts"],"mappings":";;;;;;;;;;;;;;;KAYY,oBACV,SAAS,iBACT,gBAAgB,QAAQ,+BACxB;;;;KAMU,uBAAuB;;;;KAKvB,6BAA6B,WAAW;;;;KAKxC,0BAA0B;;EAEpC;;;;;KAMU,2BAA2B;;;;;KAM3B,uBACV,UAAU,yCAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAKA,kBACL,KAAKC,gBACL,MAAM,4BACH;;;;;;;;;;qBClBM,kBAAkB;;;;;;;;qBAuFlB,qBAAqB;;;;;;KCtDtB;EACN;EAAiC,MAAM;;EACvC;EAA2B;;EAE7B;EACA,MAAM;;EAEJ;EAAoC,MAAM;;;;;;KAmCpC;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,aAAa,WAAW;;;;KAIxB,YAAY,gBAAgB,YAAY;;;;;;;;wBCzH9B,UAAU,KAAK,kBAAe;;;;;;;wBAepC,WAAW,KAAK,iBAAiB,MAAM;;;;;;;;;;;;;;;;;;wBA2BvC,eACd,MAAM,iBACJ,SAASC,kBAAgB,OAAOC,mBAAiB;;;;;;;qBAgBxC,WAAQ,KACdD,qBACJ,QAAQ;;;;;;;;;;qBAoFE,WAAQ,OACZC,gBAAY,kBACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../../../../src/express/types.d.ts","../../../../src/hono/types.d.ts","../../../../src/fastify/types.d.ts","../../../../src/koa/types.d.ts","../../../../src/h3/types.d.ts","../../../../src/types.d.ts","../../../../src/fastify/plugin.ts"],"mappings":";;;;;;;;;;;;;;;;UA+BiB;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,
|
|
1
|
+
{"version":3,"file":"plugin.d.mts","names":["Response"],"sources":["../../../../src/express/types.d.ts","../../../../src/hono/types.d.ts","../../../../src/fastify/types.d.ts","../../../../src/koa/types.d.ts","../../../../src/h3/types.d.ts","../../../../src/types.d.ts","../../../../src/fastify/plugin.ts"],"mappings":";;;;;;;;;;;;;;;;UA+BiB;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiBA,YACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;;UCzBU;;EAEf,SAAS;;;;;;;KCEC,uBAAuB;;;;KAKvB,6BAA6B,WAAW;;;;UA4BnC;;;;;;;EAOf,UACE,KAAK,gBACL,KAAK,cACL,MAAM,4BACH;;;;;;;UCzCU;;;;;;EAMf,UAAU,KAAK,SAAS,MAAM,SAAS;;;;;;;UClBxB;;;;;;EAMf,SAAS;;;;;;;;UCKM;;EAEf,SAAS;;EAET,MAAM;;EAEN,SAAS;;EAET,KAAK;;EAEL,IAAI;;;;;;;;UAqNW;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;UAGe,kBACf,UAAU;;;;EAKV;;;;;;;;;;;;EAaA,gBAAgB;;;;;;;;;;EAWhB;;;;;;;;EASA;;;;;;;;EASA;;;;;EAMA;;;;;;;;;;;;;;;;;;EAmBA,UAAU,eAAe;;;;cC9UrB,WAEA"}
|
package/dist/h3/h3.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"h3.d.mts","names":[],"sources":["../../src/h3/types.d.ts","../../src/h3/createMiddleware.ts","../../src/h3/helpers.ts"],"mappings":";;;;;;;KAOY,sBAAsB;;;;UAKjB;;;;;;EAMf,SAAS;;;;;;KAOC,kBAAkB,UAAU,oCACtC,iBAAiB,QAAQ,yBACtB;;;;KAKO,
|
|
1
|
+
{"version":3,"file":"h3.d.mts","names":["H3","H3Event","H3Event"],"sources":["../../src/h3/types.d.ts","../../src/h3/createMiddleware.ts","../../src/h3/helpers.ts"],"mappings":";;;;;;;KAOY,sBAAsB;;;;UAKjB;;;;;;EAMf,SAAS;;;;;;KAOC,kBAAkB,UAAU,oCACtC,iBAAiB,QAAQ,yBACtB;;;;KAKO,QAAQA;;;;KAKR,kBAAkBC;EAAY;;;;;;;;;;;qBCG7B,kBAAkB;;;;;;;;qBA4ElB,qBAAqB;;;;;;;;wBCtGZ,UAAU,KAAK,QAAK;;;;;;;qBAgB7B,aAAU,KAAS,OAAK,MAAQ;;;;;;;;qBAWhC,iBAAc,MAAU,kBAAgB;;;;;;;qBA6ExC,WAAQ,OAAiBC,cAAU,QAAQ;;;;;;;;;qBAmC3C,WAAQ,kBACH,oBAEf"}
|
package/dist/h3/h3.mjs
CHANGED
|
@@ -204,7 +204,7 @@ const createMiddleware = (initialOptions = {}) => {
|
|
|
204
204
|
const createRPCMiddleware = (initialOptions = {}) => {
|
|
205
205
|
const options = Object.assign({}, defaultMiddlewareOptions, { rpcPrefix: defaultRPCOptions.rpcPrefix }, initialOptions);
|
|
206
206
|
const rpcPrefix = options.rpcPrefix;
|
|
207
|
-
const prefix = rpcPrefix || "__rpc";
|
|
207
|
+
const prefix = rpcPrefix || getGlobalPrefix() || "__rpc";
|
|
208
208
|
const prefixRegex = rpcPrefix ? new RegExp(`^/${escapeRegExp(rpcPrefix)}/`) : null;
|
|
209
209
|
const prefixReplace = `/${prefix}/`;
|
|
210
210
|
return createMiddleware({
|
package/dist/h3/h3.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"h3.mjs","names":["h3Redirect","h3Redirect"],"sources":["../../src/options.ts","../../src/functionsMap.ts","../../src/constants.ts","../../src/h3/helpers.ts","../../src/h3/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","// src/h3/helpers.ts\nimport type { H3Event, Middleware } from \"h3\";\nimport { HTTPResponse, redirect as h3Redirect } from \"h3\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { ViteDevServer } from \"vite\";\nimport type { BodyResult } from \"@thednp/rpc\";\nimport type { H3App } from \"./types.d.ts\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to an h3 app.\n * Dynamically imports loadRPCConfig and registers the middleware.\n * @param app - h3 application instance\n */\nexport async function attachRPC(app: H3App) {\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\n app.use(createRPCMiddleware(options));\n}\n\n/**\n * Attaches Vite's dev server middlewares to an h3 app for development mode.\n * Uses the viteMiddleware wrapper to bridge Vite's Connect-compatible stack into h3.\n * @param app - h3 application instance\n * @param vite - Running Vite dev server\n */\nexport const attachVite = (app: H3App, vite: ViteDevServer): void => {\n app.use(viteMiddleware(vite));\n};\n\n/**\n * Creates an h3-compatible middleware from a Vite dev server middleware stack.\n * Bridges the Connect/Express middleware interface to h3's event-based request/response model.\n * Supports both Node.js and web runtimes with separate polyfill paths.\n * @param vite - Running Vite dev server\n * @returns An h3 middleware function\n */\nexport const viteMiddleware = (vite: ViteDevServer): Middleware => {\n return (event, next) =>\n new Promise((resolve) => {\n const node = event.runtime?.node;\n if (node?.req && node?.res) {\n const nodeReq = node.req;\n const nodeRes = node.res;\n // ─── Node.js runtime ─────────────────────────────────────────────\n // Forward to the real node req/res: if the Vite/Connect stack writes\n // the response, the socket is already flushed (dev-mode asset serving,\n // HMR). Connect never calls the final callback once a middleware has\n // written the response, so also settle on the response lifecycle\n // events. Stop the chain with an empty response once the socket is\n // used; the runtime's write guard prevents a second write. When the\n // stack passes through, continue to the next middleware.\n let settled = false;\n const settle = (value: unknown) => {\n // istanbul ignore if\n if (settled) return;\n settled = true;\n resolve(value);\n };\n nodeRes.once(\"close\", () => settle(new Response(null)));\n nodeRes.once(\"finish\", () => settle(new Response(null)));\n vite.middlewares(\n nodeReq as IncomingMessage,\n nodeRes as ServerResponse,\n () => {\n if (nodeRes.writableEnded || nodeRes.headersSent) {\n settle(new Response(null));\n } else {\n settle(next());\n }\n },\n );\n return;\n }\n\n // ─── Web runtime fallback ──────────────────────────────────────────\n let sent = false;\n const headers = new Headers();\n const req = {\n url: event.url.pathname + event.url.search,\n method: event.req.method,\n headers: Object.fromEntries(event.req.headers),\n } as IncomingMessage;\n const res = {\n setHeader(name: string, value: unknown) {\n headers.set(name, String(value));\n return this;\n },\n writeHead(status: number) {\n void status;\n return this;\n },\n end(body?: unknown) {\n sent = true;\n resolve(\n new HTTPResponse(body == null ? \"\" : (body as BodyInit), {\n headers,\n }),\n );\n return this;\n },\n } as ServerResponse;\n vite.middlewares(req, res, () => {\n if (!sent) resolve(next());\n });\n });\n};\n\n/**\n * Reads and parses the HTTP request body from an h3 event.\n * Supports JSON, text, urlencoded, and multipart content types.\n * @param event - h3 event object\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = async (event: H3Event): Promise<BodyResult> => {\n const contentType = event.req.headers.get(\"content-type\")?.toLowerCase() ||\n \"\";\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n const text = await event.req.text();\n if (isJSON) {\n return {\n contentType: \"application/json\",\n data: JSON.parse(text),\n } as BodyResult;\n }\n return {\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart\n ? ({ raw: text } as Record<string, unknown>)\n : isUrlEncoded\n ? Object.fromEntries(new URLSearchParams(text))\n : String(text),\n } as BodyResult;\n};\n\n/**\n * Issues an HTTP redirect. h3's `redirect()` returns an `HTTPResponse`\n * object that the handler must return (it never writes directly). Defaults\n * to `303 See Other` for convention (Post/Redirect/Get).\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n * @returns An h3 `HTTPResponse` to return from the handler\n */\nexport const redirect = (\n location: string,\n status = 303,\n): HTTPResponse => {\n return h3Redirect(\n location,\n status,\n status === 303 ? \"See Other\" : undefined,\n );\n};\n","// src/h3/createMiddleware.ts\nimport type { H3Event, Middleware } from \"h3\";\nimport type { H3MiddlewareFn, H3MiddlewareOptions } from \"./types.d.ts\";\nimport type { JsonValue } from \"@thednp/rpc\";\nimport type { RequestEvent } from \"@thednp/rpc/server\";\nimport {\n escapeRegExp,\n formatError,\n getGlobalPrefix,\n hasContentTypeMismatch,\n provideRequestContext,\n scanForServerFiles,\n} from \"@thednp/rpc/server\";\nimport { getFunctionsForPrefix } from \"../functionsMap.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\";\nimport {\n defaultMiddlewareOptions,\n defaultPrefix,\n defaultRPCOptions,\n} from \"../options.ts\";\nimport { readBody, redirect as h3Redirect } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates an h3 middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * h3 URL is normalized via `event.url` (query strings are not part of the pathname).\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns An h3 middleware function\n */\nexport const createMiddleware: H3MiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as H3MiddlewareOptions;\n\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: Middleware = async (event: H3Event, next) => {\n const url = event.url.pathname;\n\n // No need to continue when no handler provided\n if (!handler) {\n return next();\n }\n\n if (pathMatcher && !pathMatcher.test(url)) {\n return next();\n }\n\n if (prefixRegex && !prefixRegex.test(url)) {\n return next();\n }\n\n rpcPrefix = rpcPrefix || getGlobalPrefix() || defaultPrefix;\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 as unknown as { serverFiles?: \"exact\" | \"glob\" })\n .serverFiles,\n scanRoot: (options as unknown as { scanRoot?: string }).scanRoot,\n } as never);\n }\n\n return handler(event, next);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the h3 RPC middleware that routes incoming requests to registered server functions.\n * Wraps the generic createMiddleware with the RPC handler that reads the body, dispatches\n * to the matching function, and returns the JSON-serialized result.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns An h3 middleware function\n */\nexport const createRPCMiddleware: H3MiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as H3MiddlewareOptions;\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 || defaultPrefix;\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 (event: H3Event, _next?: () => unknown) => {\n const url = event.url.pathname;\n\n // Defense-in-depth: validate prefix match via escaped regex even though\n // the outer createMiddleware gates on the same prefix already.\n // istanbul ignore if\n if (prefixRegex && !prefixRegex.test(url)) {\n /* istanbul ignore next */\n return undefined;\n }\n\n // Optional origin check: reject requests whose Origin header does not\n // match the configured origin. Requests without an Origin header\n // (curl, native clients) pass through unchecked.\n const origin = options.origin;\n const requestOrigin = event.req.headers.get(\"origin\") ?? undefined;\n if (origin && requestOrigin && requestOrigin !== origin) {\n event.res.status = 403;\n return { error: REQUEST_FORBIDDEN };\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = getFunctionsForPrefix(prefix).get(functionName);\n\n if (!serverFunction) {\n event.res.status = 404;\n return { error: FUNCTION_NOT_FOUND };\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (event.req.method.toUpperCase() !== method) {\n event.res.status = 405;\n return { error: METHOD_NOT_ALLOWED };\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = event.url.searchParams.get(\"args\");\n if (raw) {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n event.res.status = 400;\n return { error: BAD_REQUEST };\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 event.req.headers.get(\"content-type\") ?? undefined,\n )\n ) {\n event.res.status = 415;\n return { error: UNSUPPORTED_MEDIA_TYPE };\n }\n const body = await readBody(event);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const requestEvent: RequestEvent = {\n request: event.req,\n response: event.res,\n nativeEvent: event,\n locals: event.context,\n functionName,\n // h3's `redirect()` returns an `HTTPResponse` (never writes directly),\n // so the bound redirect/send only record the intent; the middleware\n // uses them after the dispatch to return the response body.\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n },\n send: (status, body, headers) => {\n requestEvent.sent = { status, body, headers };\n },\n };\n const fnResult = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onClose = () => fnResult.cancel(CLIENT_DISCONNECTED);\n // The node runtime gives us the raw incoming stream for close events;\n // other runtimes have no node req, so the abort hook is skipped.\n const nodeReq = event.runtime?.node?.req;\n if (nodeReq) nodeReq.on(\"close\", onClose);\n const result = await fnResult.data;\n if (nodeReq) nodeReq.off(\"close\", onClose);\n\n if (requestEvent.redirected) {\n return h3Redirect(\n requestEvent.redirected.location,\n requestEvent.redirected.status,\n );\n }\n\n if (requestEvent.sent) {\n const { status, body, headers } = requestEvent.sent;\n event.res.status = status;\n if (headers) {\n for (const [name, value] of Object.entries(headers)) {\n event.res.headers.set(name, value);\n }\n }\n return body;\n }\n\n return { data: result };\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n event.res.status = 500;\n\n return formatError(err, isProduction);\n }\n },\n });\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"h3.mjs","names":["h3Redirect","h3Redirect"],"sources":["../../src/options.ts","../../src/functionsMap.ts","../../src/constants.ts","../../src/h3/helpers.ts","../../src/h3/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","// src/h3/helpers.ts\nimport type { H3Event, Middleware } from \"h3\";\nimport { HTTPResponse, redirect as h3Redirect } from \"h3\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { ViteDevServer } from \"vite\";\nimport type { BodyResult } from \"@thednp/rpc\";\nimport type { H3App } from \"./types.d.ts\";\nimport { createRPCMiddleware } from \"./createMiddleware.ts\";\n\n/**\n * Convenience function to load RPC config and attach the RPC middleware to an h3 app.\n * Dynamically imports loadRPCConfig and registers the middleware.\n * @param app - h3 application instance\n */\nexport async function attachRPC(app: H3App) {\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\n app.use(createRPCMiddleware(options));\n}\n\n/**\n * Attaches Vite's dev server middlewares to an h3 app for development mode.\n * Uses the viteMiddleware wrapper to bridge Vite's Connect-compatible stack into h3.\n * @param app - h3 application instance\n * @param vite - Running Vite dev server\n */\nexport const attachVite = (app: H3App, vite: ViteDevServer): void => {\n app.use(viteMiddleware(vite));\n};\n\n/**\n * Creates an h3-compatible middleware from a Vite dev server middleware stack.\n * Bridges the Connect/Express middleware interface to h3's event-based request/response model.\n * Supports both Node.js and web runtimes with separate polyfill paths.\n * @param vite - Running Vite dev server\n * @returns An h3 middleware function\n */\nexport const viteMiddleware = (vite: ViteDevServer): Middleware => {\n return (event, next) =>\n new Promise((resolve) => {\n const node = event.runtime?.node;\n if (node?.req && node?.res) {\n const nodeReq = node.req;\n const nodeRes = node.res;\n // ─── Node.js runtime ─────────────────────────────────────────────\n // Forward to the real node req/res: if the Vite/Connect stack writes\n // the response, the socket is already flushed (dev-mode asset serving,\n // HMR). Connect never calls the final callback once a middleware has\n // written the response, so also settle on the response lifecycle\n // events. Stop the chain with an empty response once the socket is\n // used; the runtime's write guard prevents a second write. When the\n // stack passes through, continue to the next middleware.\n let settled = false;\n const settle = (value: unknown) => {\n // istanbul ignore if\n if (settled) return;\n settled = true;\n resolve(value);\n };\n nodeRes.once(\"close\", () => settle(new Response(null)));\n nodeRes.once(\"finish\", () => settle(new Response(null)));\n vite.middlewares(\n nodeReq as IncomingMessage,\n nodeRes as ServerResponse,\n () => {\n if (nodeRes.writableEnded || nodeRes.headersSent) {\n settle(new Response(null));\n } else {\n settle(next());\n }\n },\n );\n return;\n }\n\n // ─── Web runtime fallback ──────────────────────────────────────────\n let sent = false;\n const headers = new Headers();\n const req = {\n url: event.url.pathname + event.url.search,\n method: event.req.method,\n headers: Object.fromEntries(event.req.headers),\n } as IncomingMessage;\n const res = {\n setHeader(name: string, value: unknown) {\n headers.set(name, String(value));\n return this;\n },\n writeHead(status: number) {\n void status;\n return this;\n },\n end(body?: unknown) {\n sent = true;\n resolve(\n new HTTPResponse(body == null ? \"\" : (body as BodyInit), {\n headers,\n }),\n );\n return this;\n },\n } as ServerResponse;\n vite.middlewares(req, res, () => {\n if (!sent) resolve(next());\n });\n });\n};\n\n/**\n * Reads and parses the HTTP request body from an h3 event.\n * Supports JSON, text, urlencoded, and multipart content types.\n * @param event - h3 event object\n * @returns A promise resolving to the parsed body with its content type\n */\nexport const readBody = async (event: H3Event): Promise<BodyResult> => {\n const contentType = event.req.headers.get(\"content-type\")?.toLowerCase() ||\n \"\";\n const isJSON = contentType.includes(\"json\");\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const isUrlEncoded = contentType.includes(\"urlencoded\");\n const text = await event.req.text();\n if (isJSON) {\n return {\n contentType: \"application/json\",\n data: JSON.parse(text),\n } as BodyResult;\n }\n return {\n contentType: isMultipart\n ? \"multipart/form-data\"\n : isUrlEncoded\n ? \"application/x-www-form-urlencoded\"\n : \"text/plain\",\n data: isMultipart\n ? ({ raw: text } as Record<string, unknown>)\n : isUrlEncoded\n ? Object.fromEntries(new URLSearchParams(text))\n : String(text),\n } as BodyResult;\n};\n\n/**\n * Issues an HTTP redirect. h3's `redirect()` returns an `HTTPResponse`\n * object that the handler must return (it never writes directly). Defaults\n * to `303 See Other` for convention (Post/Redirect/Get).\n * @param location - The URL to redirect to\n * @param status - HTTP status code, defaults to 303\n * @returns An h3 `HTTPResponse` to return from the handler\n */\nexport const redirect = (\n location: string,\n status = 303,\n): HTTPResponse => {\n return h3Redirect(\n location,\n status,\n status === 303 ? \"See Other\" : undefined,\n );\n};\n","// src/h3/createMiddleware.ts\nimport type { H3Event, Middleware } from \"h3\";\nimport type { H3MiddlewareFn, H3MiddlewareOptions } from \"./types.d.ts\";\nimport type { JsonValue } from \"@thednp/rpc\";\nimport type { RequestEvent } from \"@thednp/rpc/server\";\nimport {\n escapeRegExp,\n formatError,\n getGlobalPrefix,\n hasContentTypeMismatch,\n provideRequestContext,\n scanForServerFiles,\n} from \"@thednp/rpc/server\";\nimport { getFunctionsForPrefix } from \"../functionsMap.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\";\nimport {\n defaultMiddlewareOptions,\n defaultPrefix,\n defaultRPCOptions,\n} from \"../options.ts\";\nimport { readBody, redirect as h3Redirect } from \"./helpers.ts\";\n\nlet middlewareCount = 0;\nconst middlewareStack = new Set<string>();\n\n/**\n * Creates an h3 middleware with optional path and rpcPrefix filtering.\n * Middleware names are deduplicated. Prefix and path regexes are compiled once at creation time.\n * h3 URL is normalized via `event.url` (query strings are not part of the pathname).\n * @param initialOptions - Options for rpcPrefix, path matching, and the handler function\n * @returns An h3 middleware function\n */\nexport const createMiddleware: H3MiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n initialOptions,\n ) as H3MiddlewareOptions;\n\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: Middleware = async (event: H3Event, next) => {\n const url = event.url.pathname;\n\n // No need to continue when no handler provided\n if (!handler) {\n return next();\n }\n\n if (pathMatcher && !pathMatcher.test(url)) {\n return next();\n }\n\n if (prefixRegex && !prefixRegex.test(url)) {\n return next();\n }\n\n rpcPrefix = rpcPrefix || getGlobalPrefix() || defaultPrefix;\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 as unknown as { serverFiles?: \"exact\" | \"glob\" })\n .serverFiles,\n scanRoot: (options as unknown as { scanRoot?: string }).scanRoot,\n } as never);\n }\n\n return handler(event, next);\n };\n\n Object.defineProperty(middlewareHandler, \"name\", {\n value: name,\n });\n\n return middlewareHandler;\n};\n\n/**\n * Creates the h3 RPC middleware that routes incoming requests to registered server functions.\n * Wraps the generic createMiddleware with the RPC handler that reads the body, dispatches\n * to the matching function, and returns the JSON-serialized result.\n * @param initialOptions - Options including rpcPrefix for URL routing\n * @returns An h3 middleware function\n */\nexport const createRPCMiddleware: H3MiddlewareFn = (initialOptions = {}) => {\n const options = Object.assign(\n {},\n defaultMiddlewareOptions,\n { rpcPrefix: defaultRPCOptions.rpcPrefix },\n initialOptions,\n ) as H3MiddlewareOptions;\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 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 (event: H3Event, _next?: () => unknown) => {\n const url = event.url.pathname;\n\n // Defense-in-depth: validate prefix match via escaped regex even though\n // the outer createMiddleware gates on the same prefix already.\n // istanbul ignore if\n if (prefixRegex && !prefixRegex.test(url)) {\n /* istanbul ignore next */\n return undefined;\n }\n\n // Optional origin check: reject requests whose Origin header does not\n // match the configured origin. Requests without an Origin header\n // (curl, native clients) pass through unchecked.\n const origin = options.origin;\n const requestOrigin = event.req.headers.get(\"origin\") ?? undefined;\n if (origin && requestOrigin && requestOrigin !== origin) {\n event.res.status = 403;\n return { error: REQUEST_FORBIDDEN };\n }\n\n const functionName = url.replace(prefixReplace, \"\");\n const serverFunction = getFunctionsForPrefix(prefix).get(functionName);\n\n if (!serverFunction) {\n event.res.status = 404;\n return { error: FUNCTION_NOT_FOUND };\n }\n\n try {\n const method = serverFunction.options?.method || \"POST\";\n if (event.req.method.toUpperCase() !== method) {\n event.res.status = 405;\n return { error: METHOD_NOT_ALLOWED };\n }\n\n let args: JsonValue[] = [];\n if (method === \"GET\") {\n const raw = event.url.searchParams.get(\"args\");\n if (raw) {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n event.res.status = 400;\n return { error: BAD_REQUEST };\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 event.req.headers.get(\"content-type\") ?? undefined,\n )\n ) {\n event.res.status = 415;\n return { error: UNSUPPORTED_MEDIA_TYPE };\n }\n const body = await readBody(event);\n args = Array.isArray(body.data)\n ? body.data as JsonValue[]\n : [body.data as JsonValue];\n }\n const requestEvent: RequestEvent = {\n request: event.req,\n response: event.res,\n nativeEvent: event,\n locals: event.context,\n functionName,\n // h3's `redirect()` returns an `HTTPResponse` (never writes directly),\n // so the bound redirect/send only record the intent; the middleware\n // uses them after the dispatch to return the response body.\n redirect: (location, status = 303) => {\n requestEvent.redirected = { location, status };\n },\n send: (status, body, headers) => {\n requestEvent.sent = { status, body, headers };\n },\n };\n const fnResult = provideRequestContext(\n requestEvent,\n () => serverFunction.handler(...args),\n );\n const onClose = () => fnResult.cancel(CLIENT_DISCONNECTED);\n // The node runtime gives us the raw incoming stream for close events;\n // other runtimes have no node req, so the abort hook is skipped.\n const nodeReq = event.runtime?.node?.req;\n if (nodeReq) nodeReq.on(\"close\", onClose);\n const result = await fnResult.data;\n if (nodeReq) nodeReq.off(\"close\", onClose);\n\n if (requestEvent.redirected) {\n return h3Redirect(\n requestEvent.redirected.location,\n requestEvent.redirected.status,\n );\n }\n\n if (requestEvent.sent) {\n const { status, body, headers } = requestEvent.sent;\n event.res.status = status;\n if (headers) {\n for (const [name, value] of Object.entries(headers)) {\n event.res.headers.set(name, value);\n }\n }\n return body;\n }\n\n return { data: result };\n } catch (err) {\n console.error(String(err));\n const isProduction = process.env.NODE_ENV === \"production\";\n event.res.status = 500;\n\n return formatError(err, isProduction);\n }\n },\n });\n};\n"],"mappings":"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"}
|
|
@@ -94,10 +94,19 @@ type InnerModReturn<T extends JsonValue> = {
|
|
|
94
94
|
export declare const handleResponse: <R extends JsonValue>(response: Response) => Promise<R | void>;
|
|
95
95
|
/**
|
|
96
96
|
* Unwraps the `{ data }` envelope from a parsed RPC response body.
|
|
97
|
-
*
|
|
98
|
-
*
|
|
97
|
+
*
|
|
98
|
+
* Error handling matches `handleResponse` so both helpers in this module agree
|
|
99
|
+
* on the contract: a **top-level** `error` key (which the server only emits for
|
|
100
|
+
* 404/405/415/400/500) throws, while a `{ data: { error } }` body resolves
|
|
101
|
+
* normally — that shape is the documented "validation-as-data" contract where a
|
|
102
|
+
* 200 carries the validation outcome as its result.
|
|
103
|
+
*
|
|
104
|
+
* Discriminating on `error` present **and** `data` absent is what keeps those
|
|
105
|
+
* two cases apart. Checking `res.ok` first is still recommended, since this
|
|
106
|
+
* helper is status-code agnostic by design.
|
|
99
107
|
* @param json - Parsed JSON response body
|
|
100
108
|
* @returns The unwrapped response data
|
|
109
|
+
* @throws When the body carries a top-level `error` and no `data`
|
|
101
110
|
* @example
|
|
102
111
|
* ```ts
|
|
103
112
|
* const response = await fetch("/__rpc/greet", { method: "POST", ... });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.mts","names":[],"sources":["../../src/types.d.ts","../../src/client-helpers.ts"],"mappings":";;;;;;;;;;;;;;;KAkEY;;;;KASA;;;;;KA+CA;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,aAAa,WAAW;;;;KAIxB,YAAY,gBAAgB,YAAY;;;;;;KAgDxC,eACV,cAAc,YAAY,WAC1B,UAAU,iBACJ,MAAM;;EAEZ,MAAM,QAAQ;;EAEd,SAAS;;;;;;UAuLM;;;;;EAKf;;;;;EAKA,aAAa;;;;;;;EAOb,aAAa;;;;;;KAOH,eAAe,UAAU;;EAEnC,MAAM,QAAQ;;EAEd,SAAS;;;;;;;;;;;;qBC7XE,iBAAwB,UAAU,WAAS,UAC5C,aACT,QAAQ
|
|
1
|
+
{"version":3,"file":"helpers.d.mts","names":[],"sources":["../../src/types.d.ts","../../src/client-helpers.ts"],"mappings":";;;;;;;;;;;;;;;KAkEY;;;;KASA;;;;;KA+CA;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,aAAa,WAAW;;;;KAIxB,YAAY,gBAAgB,YAAY;;;;;;KAgDxC,eACV,cAAc,YAAY,WAC1B,UAAU,iBACJ,MAAM;;EAEZ,MAAM,QAAQ;;EAEd,SAAS;;;;;;UAuLM;;;;;EAKf;;;;;EAKA,aAAa;;;;;;;EAOb,aAAa;;;;;;KAOH,eAAe,UAAU;;EAEnC,MAAM,QAAQ;;EAEd,SAAS;;;;;;;;;;;;qBC7XE,iBAAwB,UAAU,WAAS,UAC5C,aACT,QAAQ;;;;;;;;;;;;;;;;;;;;;;;qBAkCE,iBAAkB,GAAC,kBAAkB;;;;;;;;;;;;;;;;;;;;wBA0HlC,cAAc,UAAU,WAAW,UAAU,WAC3D,gBACA,cACA,UAAU,QAAQ,eACjB,eAAe,GAAG;;;;;;;;;;;;;;qBAiBR,cAAe,UAAU,WAAS,MACvC,UAAQ,SACL,aAAW,aACP,aAAW,gBACV,cACF,4BAEX,eAAe"}
|
package/dist/helpers/helpers.mjs
CHANGED
|
@@ -22,10 +22,19 @@ const handleResponse = async (response) => {
|
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
24
|
* Unwraps the `{ data }` envelope from a parsed RPC response body.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
*
|
|
26
|
+
* Error handling matches `handleResponse` so both helpers in this module agree
|
|
27
|
+
* on the contract: a **top-level** `error` key (which the server only emits for
|
|
28
|
+
* 404/405/415/400/500) throws, while a `{ data: { error } }` body resolves
|
|
29
|
+
* normally — that shape is the documented "validation-as-data" contract where a
|
|
30
|
+
* 200 carries the validation outcome as its result.
|
|
31
|
+
*
|
|
32
|
+
* Discriminating on `error` present **and** `data` absent is what keeps those
|
|
33
|
+
* two cases apart. Checking `res.ok` first is still recommended, since this
|
|
34
|
+
* helper is status-code agnostic by design.
|
|
27
35
|
* @param json - Parsed JSON response body
|
|
28
36
|
* @returns The unwrapped response data
|
|
37
|
+
* @throws When the body carries a top-level `error` and no `data`
|
|
29
38
|
* @example
|
|
30
39
|
* ```ts
|
|
31
40
|
* const response = await fetch("/__rpc/greet", { method: "POST", ... });
|
|
@@ -34,7 +43,11 @@ const handleResponse = async (response) => {
|
|
|
34
43
|
* ```
|
|
35
44
|
*/
|
|
36
45
|
const unwrapEnvelope = (json) => {
|
|
37
|
-
if (json !== null && typeof json === "object"
|
|
46
|
+
if (json !== null && typeof json === "object") {
|
|
47
|
+
const envelope = json;
|
|
48
|
+
if (!("data" in envelope) && "error" in envelope) throw new Error(String(envelope.error));
|
|
49
|
+
if ("data" in envelope) return envelope.data;
|
|
50
|
+
}
|
|
38
51
|
return json;
|
|
39
52
|
};
|
|
40
53
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.mjs","names":[],"sources":["../../src/constants.ts","../../src/client-helpers.ts"],"sourcesContent":["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 Client-side helper utilities. Exports `handleResponse` for processing fetch responses and `innerModule` for creating AbortController-bound RPC fetch calls. This module is bundled into the generated client modules — keep it free of server-only code. */\nimport type {\n ClientFunction,\n Credentials,\n InnerModReturn,\n JsonArray,\n JsonValue,\n StubOptions,\n} from \"./types.d.ts\";\nimport { FETCH_ERROR_PREFIX, REQUEST_CANCELLED } from \"./constants.ts\";\n\n/**\n * Processes an HTTP fetch response from the RPC server.\n * On HTTP 499 or 408 (client cancellation), logs a warning and returns undefined.\n * On other error statuses, throws a Fetch error.\n * On success, parses JSON and returns `result.data` — or throws if `result.error` is set.\n * @param response - Fetch Response object from the RPC endpoint\n * @returns The response data, or void on cancellation\n */\nexport const handleResponse = async <R extends JsonValue>(\n response: Response,\n): Promise<R | void> => {\n if (!response.ok) {\n if (response.status === 499 || response.status === 408) {\n return console.warn(REQUEST_CANCELLED);\n }\n throw new Error(FETCH_ERROR_PREFIX + response.statusText);\n }\n const result = await response.json();\n if (result.error) throw new Error(result.error);\n return result.data as R;\n};\n\n/**\n * Unwraps the `{ data }` envelope from a parsed RPC response body.\n * When the input is an object with a `data` property, returns `data`.\n * Otherwise returns the input as-is (for direct responses without the envelope).\n * @param json - Parsed JSON response body\n * @returns The unwrapped response data\n * @example\n * ```ts\n * const response = await fetch(\"/__rpc/greet\", { method: \"POST\", ... });\n * const body = await response.json();\n * const greeting = unwrapEnvelope<string>(body); // \"Hello, world!\"\n * ```\n */\nexport const unwrapEnvelope = <T>(json: unknown): T => {\n if (json !== null && typeof json === \"object\" && \"data\" in json) {\n return (json as { data: T }).data;\n }\n return json as T;\n};\n\n/**\n * Low-level stub factory used by both `getClientStub` and the auto-generated\n * modules (`src/getClientModules.ts:73`). Keeps body/header mapping in one\n * place so `innerModule` stays thin.\n */\nconst makeStub = <T extends JsonArray, R extends JsonValue>(\n prefix: string,\n name: string,\n options: Partial<StubOptions> = {},\n): ClientFunction<T, R> => {\n const method = (options.method ?? \"POST\") as \"GET\" | \"POST\";\n const credentials = (options.credentials ?? \"same-origin\") as Credentials;\n const contentType = (options.contentType ?? \"application/json\") as string;\n if (method === \"GET\") {\n const headers = {} as HeadersInit;\n return (<TArgs extends T, Res extends R>(\n ...args: TArgs\n ): InnerModReturn<Res> => {\n const json = JSON.stringify(args);\n return innerModule<Res>(\n json as BodyInit,\n headers,\n credentials,\n prefix,\n name,\n method,\n );\n }) as ClientFunction<T, R>;\n }\n switch (contentType) {\n case \"text/plain\": {\n const headers = { \"Content-Type\": \"text/plain\" } as HeadersInit;\n return (<TArgs extends T, Res extends R>(\n ...args: TArgs\n ): InnerModReturn<Res> =>\n innerModule(\n (args[0] as string) as BodyInit,\n headers,\n credentials,\n prefix,\n name,\n method,\n )) as ClientFunction<T, R>;\n }\n case \"application/x-www-form-urlencoded\": {\n const headers: HeadersInit = {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n };\n return (<TArgs extends T, Res extends R>(\n ...args: TArgs\n ): InnerModReturn<Res> =>\n innerModule(\n new URLSearchParams(args[0] as Record<string, string>)\n .toString() as BodyInit,\n headers,\n credentials,\n prefix,\n name,\n method,\n )) as ClientFunction<T, R>;\n }\n case \"multipart/form-data\": {\n const headers: HeadersInit = {};\n return (<TArgs extends T, Res extends R>(\n ...args: TArgs\n ): InnerModReturn<Res> =>\n innerModule(\n args[0] as BodyInit,\n headers,\n credentials,\n prefix,\n name,\n method,\n )) as ClientFunction<T, R>;\n }\n default: {\n const headers: HeadersInit = { \"Content-Type\": \"application/json\" };\n return (<TArgs extends T, Res extends R>(\n ...args: TArgs\n ): InnerModReturn<Res> =>\n innerModule(\n JSON.stringify(args) as BodyInit,\n headers,\n credentials,\n prefix,\n name,\n method,\n )) as ClientFunction<T, R>;\n }\n }\n};\n\n/**\n * Creates a typed client stub for any prefix — the manual counterpart to the\n * auto-generated `public:rpc` stubs. Useful for privileged prefixes like\n * `admin:rpc` that are not emitted in the public bundle.\n * The stub has the same `{data,cancel}` shape and cancellation/error handling\n * as generated stubs, and is code-splittable: `const adminGetUser = getClientStub(\"admin:rpc\",\"get-user\")`\n * should be `await import`-ed only inside `/admin` routes so the `admin:rpc`\n * literal never appears in the public chunk.\n * @param prefix - RPC prefix (e.g. \"admin:rpc\")\n * @param name - Registered function name\n * @param options - Optional `method`, `credentials`, `contentType`\n * @returns Client stub `(...args) => {data,cancel}`\n * @example\n * import { getClientStub } from \"@thednp/rpc/helpers\";\n * const adminGetUser = getClientStub(\"admin:rpc\",\"get-user\");\n * const {data,cancel} = adminGetUser(\"123\");\n * @example\n * const adminStats = getClientStub(\"admin:rpc\",\"stats\", { method: \"GET\" });\n */\nexport function getClientStub<T extends JsonArray, R extends JsonValue>(\n prefix: string,\n name: string,\n options?: Partial<StubOptions>,\n): ClientFunction<T, R> {\n return makeStub<T, R>(prefix, name, options);\n}\n\n/**\n * Creates an AbortController-bound fetch call for a single RPC function.\n * Used by the auto-generated client modules to issue HTTP requests with cancellation support.\n * GET requests carry arguments as an `?args=` JSON query parameter, since a fetch\n * request body is not allowed on GET.\n * @param body - Serialized request body (JSON string or raw text)\n * @param headers - HTTP headers (Content-Type, etc.)\n * @param credentials - Fetch credentials policy (\"same-origin\", \"include\", or \"omit\")\n * @param prefix - RPC endpoint prefix (e.g. \"__rpc\")\n * @param name - Registered server function name\n * @param method - HTTP method to use, \"POST\" by default\n * @returns An object with `data` (promise resolving to the server response) and `cancel` (abort function)\n */\nexport const innerModule = <R extends JsonValue>(\n body: BodyInit,\n headers: HeadersInit,\n credentials: Credentials,\n prefix: string,\n name: string,\n method?: \"GET\" | \"POST\",\n): InnerModReturn<R> => {\n const controller = new AbortController();\n const cancel = (reason: string) => controller.abort(reason);\n\n const fetcher = async () => {\n try {\n const isGet = method === \"GET\";\n const url = isGet\n ? `/${prefix}/${name}?args=${encodeURIComponent(String(body))}`\n : `/${prefix}/${name}`;\n const response = await fetch(url, {\n method: isGet ? \"GET\" : \"POST\",\n headers,\n credentials,\n body: isGet ? undefined : body,\n signal: controller.signal,\n });\n return await handleResponse<R>(response);\n } catch (err) {\n throw err;\n }\n };\n\n return {\n data: fetcher(),\n cancel,\n };\n};\n"],"mappings":";AAEA,MAAa,oBAAoB;AAEjC,MAAa,qBAAqB;;;;;;;;;;;ACelC,MAAa,iBAAiB,OAC5B,aACsB;CACtB,IAAI,CAAC,SAAS,IAAI;EAChB,IAAI,SAAS,WAAW,OAAO,SAAS,WAAW,KACjD,OAAO,QAAQ,KAAK,iBAAiB;EAEvC,MAAM,IAAI,MAAM,qBAAqB,SAAS,UAAU;CAC1D;CACA,MAAM,SAAS,MAAM,SAAS,KAAK;CACnC,IAAI,OAAO,OAAO,MAAM,IAAI,MAAM,OAAO,KAAK;CAC9C,OAAO,OAAO;AAChB;;;;;;;;;;;;;;AAeA,MAAa,kBAAqB,SAAqB;CACrD,IAAI,SAAS,QAAQ,OAAO,SAAS,YAAY,UAAU,MACzD,OAAQ,KAAqB;CAE/B,OAAO;AACT;;;;;;AAOA,MAAM,YACJ,QACA,MACA,UAAgC,CAAC,MACR;CACzB,MAAM,SAAU,QAAQ,UAAU;CAClC,MAAM,cAAe,QAAQ,eAAe;CAC5C,MAAM,cAAe,QAAQ,eAAe;CAC5C,IAAI,WAAW,OAAO;EACpB,MAAM,UAAU,CAAC;EACjB,SACE,GAAG,SACqB;GACxB,MAAM,OAAO,KAAK,UAAU,IAAI;GAChC,OAAO,YACL,MACA,SACA,aACA,QACA,MACA,MACF;EACF;CACF;CACA,QAAQ,aAAR;EACE,KAAK,cAAc;GACjB,MAAM,UAAU,EAAE,gBAAgB,aAAa;GAC/C,SACE,GAAG,SAEH,YACG,KAAK,IACN,SACA,aACA,QACA,MACA,MACF;EACJ;EACA,KAAK,qCAAqC;GACxC,MAAM,UAAuB,EAC3B,gBAAgB,oCAClB;GACA,SACE,GAAG,SAEH,YACE,IAAI,gBAAgB,KAAK,EAA4B,CAAC,CACnD,SAAS,GACZ,SACA,aACA,QACA,MACA,MACF;EACJ;EACA,KAAK,uBAAuB;GAC1B,MAAM,UAAuB,CAAC;GAC9B,SACE,GAAG,SAEH,YACE,KAAK,IACL,SACA,aACA,QACA,MACA,MACF;EACJ;EACA,SAAS;GACP,MAAM,UAAuB,EAAE,gBAAgB,mBAAmB;GAClE,SACE,GAAG,SAEH,YACE,KAAK,UAAU,IAAI,GACnB,SACA,aACA,QACA,MACA,MACF;EACJ;CACF;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,cACd,QACA,MACA,SACsB;CACtB,OAAO,SAAe,QAAQ,MAAM,OAAO;AAC7C;;;;;;;;;;;;;;AAeA,MAAa,eACX,MACA,SACA,aACA,QACA,MACA,WACsB;CACtB,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,UAAU,WAAmB,WAAW,MAAM,MAAM;CAE1D,MAAM,UAAU,YAAY;EAC1B,IAAI;GACF,MAAM,QAAQ,WAAW;GACzB,MAAM,MAAM,QACR,IAAI,OAAO,GAAG,KAAK,QAAQ,mBAAmB,OAAO,IAAI,CAAC,MAC1D,IAAI,OAAO,GAAG;GAClB,MAAM,WAAW,MAAM,MAAM,KAAK;IAChC,QAAQ,QAAQ,QAAQ;IACxB;IACA;IACA,MAAM,QAAQ,KAAA,IAAY;IAC1B,QAAQ,WAAW;GACrB,CAAC;GACD,OAAO,MAAM,eAAkB,QAAQ;EACzC,SAAS,KAAK;GACZ,MAAM;EACR;CACF;CAEA,OAAO;EACL,MAAM,QAAQ;EACd;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"helpers.mjs","names":[],"sources":["../../src/constants.ts","../../src/client-helpers.ts"],"sourcesContent":["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 Client-side helper utilities. Exports `handleResponse` for processing fetch responses and `innerModule` for creating AbortController-bound RPC fetch calls. This module is bundled into the generated client modules — keep it free of server-only code. */\nimport type {\n ClientFunction,\n Credentials,\n InnerModReturn,\n JsonArray,\n JsonValue,\n StubOptions,\n} from \"./types.d.ts\";\nimport { FETCH_ERROR_PREFIX, REQUEST_CANCELLED } from \"./constants.ts\";\n\n/**\n * Processes an HTTP fetch response from the RPC server.\n * On HTTP 499 or 408 (client cancellation), logs a warning and returns undefined.\n * On other error statuses, throws a Fetch error.\n * On success, parses JSON and returns `result.data` — or throws if `result.error` is set.\n * @param response - Fetch Response object from the RPC endpoint\n * @returns The response data, or void on cancellation\n */\nexport const handleResponse = async <R extends JsonValue>(\n response: Response,\n): Promise<R | void> => {\n if (!response.ok) {\n if (response.status === 499 || response.status === 408) {\n return console.warn(REQUEST_CANCELLED);\n }\n throw new Error(FETCH_ERROR_PREFIX + response.statusText);\n }\n const result = await response.json();\n if (result.error) throw new Error(result.error);\n return result.data as R;\n};\n\n/**\n * Unwraps the `{ data }` envelope from a parsed RPC response body.\n *\n * Error handling matches `handleResponse` so both helpers in this module agree\n * on the contract: a **top-level** `error` key (which the server only emits for\n * 404/405/415/400/500) throws, while a `{ data: { error } }` body resolves\n * normally — that shape is the documented \"validation-as-data\" contract where a\n * 200 carries the validation outcome as its result.\n *\n * Discriminating on `error` present **and** `data` absent is what keeps those\n * two cases apart. Checking `res.ok` first is still recommended, since this\n * helper is status-code agnostic by design.\n * @param json - Parsed JSON response body\n * @returns The unwrapped response data\n * @throws When the body carries a top-level `error` and no `data`\n * @example\n * ```ts\n * const response = await fetch(\"/__rpc/greet\", { method: \"POST\", ... });\n * const body = await response.json();\n * const greeting = unwrapEnvelope<string>(body); // \"Hello, world!\"\n * ```\n */\nexport const unwrapEnvelope = <T>(json: unknown): T => {\n if (json !== null && typeof json === \"object\") {\n const envelope = json as { data?: T; error?: unknown };\n if (!(\"data\" in envelope) && \"error\" in envelope) {\n throw new Error(String(envelope.error));\n }\n if (\"data\" in envelope) return envelope.data as T;\n }\n return json as T;\n};\n\n/**\n * Low-level stub factory used by both `getClientStub` and the auto-generated\n * modules (`src/getClientModules.ts:73`). Keeps body/header mapping in one\n * place so `innerModule` stays thin.\n */\nconst makeStub = <T extends JsonArray, R extends JsonValue>(\n prefix: string,\n name: string,\n options: Partial<StubOptions> = {},\n): ClientFunction<T, R> => {\n const method = (options.method ?? \"POST\") as \"GET\" | \"POST\";\n const credentials = (options.credentials ?? \"same-origin\") as Credentials;\n const contentType = (options.contentType ?? \"application/json\") as string;\n if (method === \"GET\") {\n const headers = {} as HeadersInit;\n return (<TArgs extends T, Res extends R>(\n ...args: TArgs\n ): InnerModReturn<Res> => {\n const json = JSON.stringify(args);\n return innerModule<Res>(\n json as BodyInit,\n headers,\n credentials,\n prefix,\n name,\n method,\n );\n }) as ClientFunction<T, R>;\n }\n switch (contentType) {\n case \"text/plain\": {\n const headers = { \"Content-Type\": \"text/plain\" } as HeadersInit;\n return (<TArgs extends T, Res extends R>(\n ...args: TArgs\n ): InnerModReturn<Res> =>\n innerModule(\n (args[0] as string) as BodyInit,\n headers,\n credentials,\n prefix,\n name,\n method,\n )) as ClientFunction<T, R>;\n }\n case \"application/x-www-form-urlencoded\": {\n const headers: HeadersInit = {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n };\n return (<TArgs extends T, Res extends R>(\n ...args: TArgs\n ): InnerModReturn<Res> =>\n innerModule(\n new URLSearchParams(args[0] as Record<string, string>)\n .toString() as BodyInit,\n headers,\n credentials,\n prefix,\n name,\n method,\n )) as ClientFunction<T, R>;\n }\n case \"multipart/form-data\": {\n const headers: HeadersInit = {};\n return (<TArgs extends T, Res extends R>(\n ...args: TArgs\n ): InnerModReturn<Res> =>\n innerModule(\n args[0] as BodyInit,\n headers,\n credentials,\n prefix,\n name,\n method,\n )) as ClientFunction<T, R>;\n }\n default: {\n const headers: HeadersInit = { \"Content-Type\": \"application/json\" };\n return (<TArgs extends T, Res extends R>(\n ...args: TArgs\n ): InnerModReturn<Res> =>\n innerModule(\n JSON.stringify(args) as BodyInit,\n headers,\n credentials,\n prefix,\n name,\n method,\n )) as ClientFunction<T, R>;\n }\n }\n};\n\n/**\n * Creates a typed client stub for any prefix — the manual counterpart to the\n * auto-generated `public:rpc` stubs. Useful for privileged prefixes like\n * `admin:rpc` that are not emitted in the public bundle.\n * The stub has the same `{data,cancel}` shape and cancellation/error handling\n * as generated stubs, and is code-splittable: `const adminGetUser = getClientStub(\"admin:rpc\",\"get-user\")`\n * should be `await import`-ed only inside `/admin` routes so the `admin:rpc`\n * literal never appears in the public chunk.\n * @param prefix - RPC prefix (e.g. \"admin:rpc\")\n * @param name - Registered function name\n * @param options - Optional `method`, `credentials`, `contentType`\n * @returns Client stub `(...args) => {data,cancel}`\n * @example\n * import { getClientStub } from \"@thednp/rpc/helpers\";\n * const adminGetUser = getClientStub(\"admin:rpc\",\"get-user\");\n * const {data,cancel} = adminGetUser(\"123\");\n * @example\n * const adminStats = getClientStub(\"admin:rpc\",\"stats\", { method: \"GET\" });\n */\nexport function getClientStub<T extends JsonArray, R extends JsonValue>(\n prefix: string,\n name: string,\n options?: Partial<StubOptions>,\n): ClientFunction<T, R> {\n return makeStub<T, R>(prefix, name, options);\n}\n\n/**\n * Creates an AbortController-bound fetch call for a single RPC function.\n * Used by the auto-generated client modules to issue HTTP requests with cancellation support.\n * GET requests carry arguments as an `?args=` JSON query parameter, since a fetch\n * request body is not allowed on GET.\n * @param body - Serialized request body (JSON string or raw text)\n * @param headers - HTTP headers (Content-Type, etc.)\n * @param credentials - Fetch credentials policy (\"same-origin\", \"include\", or \"omit\")\n * @param prefix - RPC endpoint prefix (e.g. \"__rpc\")\n * @param name - Registered server function name\n * @param method - HTTP method to use, \"POST\" by default\n * @returns An object with `data` (promise resolving to the server response) and `cancel` (abort function)\n */\nexport const innerModule = <R extends JsonValue>(\n body: BodyInit,\n headers: HeadersInit,\n credentials: Credentials,\n prefix: string,\n name: string,\n method?: \"GET\" | \"POST\",\n): InnerModReturn<R> => {\n const controller = new AbortController();\n const cancel = (reason: string) => controller.abort(reason);\n\n const fetcher = async () => {\n try {\n const isGet = method === \"GET\";\n const url = isGet\n ? `/${prefix}/${name}?args=${encodeURIComponent(String(body))}`\n : `/${prefix}/${name}`;\n const response = await fetch(url, {\n method: isGet ? \"GET\" : \"POST\",\n headers,\n credentials,\n body: isGet ? undefined : body,\n signal: controller.signal,\n });\n return await handleResponse<R>(response);\n } catch (err) {\n throw err;\n }\n };\n\n return {\n data: fetcher(),\n cancel,\n };\n};\n"],"mappings":";AAEA,MAAa,oBAAoB;AAEjC,MAAa,qBAAqB;;;;;;;;;;;ACelC,MAAa,iBAAiB,OAC5B,aACsB;CACtB,IAAI,CAAC,SAAS,IAAI;EAChB,IAAI,SAAS,WAAW,OAAO,SAAS,WAAW,KACjD,OAAO,QAAQ,KAAK,iBAAiB;EAEvC,MAAM,IAAI,MAAM,qBAAqB,SAAS,UAAU;CAC1D;CACA,MAAM,SAAS,MAAM,SAAS,KAAK;CACnC,IAAI,OAAO,OAAO,MAAM,IAAI,MAAM,OAAO,KAAK;CAC9C,OAAO,OAAO;AAChB;;;;;;;;;;;;;;;;;;;;;;;AAwBA,MAAa,kBAAqB,SAAqB;CACrD,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU;EAC7C,MAAM,WAAW;EACjB,IAAI,EAAE,UAAU,aAAa,WAAW,UACtC,MAAM,IAAI,MAAM,OAAO,SAAS,KAAK,CAAC;EAExC,IAAI,UAAU,UAAU,OAAO,SAAS;CAC1C;CACA,OAAO;AACT;;;;;;AAOA,MAAM,YACJ,QACA,MACA,UAAgC,CAAC,MACR;CACzB,MAAM,SAAU,QAAQ,UAAU;CAClC,MAAM,cAAe,QAAQ,eAAe;CAC5C,MAAM,cAAe,QAAQ,eAAe;CAC5C,IAAI,WAAW,OAAO;EACpB,MAAM,UAAU,CAAC;EACjB,SACE,GAAG,SACqB;GACxB,MAAM,OAAO,KAAK,UAAU,IAAI;GAChC,OAAO,YACL,MACA,SACA,aACA,QACA,MACA,MACF;EACF;CACF;CACA,QAAQ,aAAR;EACE,KAAK,cAAc;GACjB,MAAM,UAAU,EAAE,gBAAgB,aAAa;GAC/C,SACE,GAAG,SAEH,YACG,KAAK,IACN,SACA,aACA,QACA,MACA,MACF;EACJ;EACA,KAAK,qCAAqC;GACxC,MAAM,UAAuB,EAC3B,gBAAgB,oCAClB;GACA,SACE,GAAG,SAEH,YACE,IAAI,gBAAgB,KAAK,EAA4B,CAAC,CACnD,SAAS,GACZ,SACA,aACA,QACA,MACA,MACF;EACJ;EACA,KAAK,uBAAuB;GAC1B,MAAM,UAAuB,CAAC;GAC9B,SACE,GAAG,SAEH,YACE,KAAK,IACL,SACA,aACA,QACA,MACA,MACF;EACJ;EACA,SAAS;GACP,MAAM,UAAuB,EAAE,gBAAgB,mBAAmB;GAClE,SACE,GAAG,SAEH,YACE,KAAK,UAAU,IAAI,GACnB,SACA,aACA,QACA,MACA,MACF;EACJ;CACF;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,cACd,QACA,MACA,SACsB;CACtB,OAAO,SAAe,QAAQ,MAAM,OAAO;AAC7C;;;;;;;;;;;;;;AAeA,MAAa,eACX,MACA,SACA,aACA,QACA,MACA,WACsB;CACtB,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,UAAU,WAAmB,WAAW,MAAM,MAAM;CAE1D,MAAM,UAAU,YAAY;EAC1B,IAAI;GACF,MAAM,QAAQ,WAAW;GACzB,MAAM,MAAM,QACR,IAAI,OAAO,GAAG,KAAK,QAAQ,mBAAmB,OAAO,IAAI,CAAC,MAC1D,IAAI,OAAO,GAAG;GAClB,MAAM,WAAW,MAAM,MAAM,KAAK;IAChC,QAAQ,QAAQ,QAAQ;IACxB;IACA;IACA,MAAM,QAAQ,KAAA,IAAY;IAC1B,QAAQ,WAAW;GACrB,CAAC;GACD,OAAO,MAAM,eAAkB,QAAQ;EACzC,SAAS,KAAK;GACZ,MAAM;EACR;CACF;CAEA,OAAO;EACL,MAAM,QAAQ;EACd;CACF;AACF"}
|
package/dist/hono/hono.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono.d.mts","names":[],"sources":["../../src/hono/types.d.ts","../../src/hono/createMiddleware.ts","../../src/hono/helpers.ts"],"mappings":";;;;;;;;;;;KAOY,mBAAmB;EAAoB;;;;;KAKvC,wBAAwB;;;;UAKnB;;EAEf,SAAS;;;;;;KAOC,oBAAoB,UAAU,sCACxC,iBAAiB,QAAQ,kBAAkB,QACxC;;;;;;;;;;qBCkBQ,kBAAkB;;;;;;;;qBAgFlB,qBAAqB;;;;;;;;wBC3GZ,UAAU,
|
|
1
|
+
{"version":3,"file":"hono.d.mts","names":["Hono","createMiddleware"],"sources":["../../src/hono/types.d.ts","../../src/hono/createMiddleware.ts","../../src/hono/helpers.ts"],"mappings":";;;;;;;;;;;KAOY,mBAAmB;EAAoB;;;;;KAKvC,wBAAwB;;;;UAKnB;;EAEf,SAAS;;;;;;KAOC,oBAAoB,UAAU,sCACxC,iBAAiB,QAAQ,kBAAkB,QACxC;;;;;;;;;;qBCkBQ,kBAAkB;;;;;;;;qBAgFlB,qBAAqB;;;;;;;;wBC3GZ,UAAU,KAAKA,SAAI;;;;;;;qBAgB5B,aAAU,KAASA,QAAI,MAAQ;;;;;;;;;qBAY/B,iBAAc,MACnB,kBACL,kBAAkBC;EAAmB,UAAU;;;;;;;;qBAgDrC,WAAQ,GAChB,YACF,QAAQ;;;;;;;;;;;qBA4DE,WAAQ,GAChB,SAAO,kBACM,SACR,uBACP"}
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/express/types.d.ts","../src/hono/types.d.ts","../src/fastify/types.d.ts","../src/koa/types.d.ts","../src/h3/types.d.ts","../src/types.d.ts","../src/index.ts"],"mappings":";;;;;;;;;;;;;;;;KAgBY,
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":["MiddlewareOptions","RpcPluginOptions","Response","RpcPluginOptions","MiddlewareOptions","MiddlewareOptions","RpcPluginOptions","MiddlewareOptions","RpcPluginOptions","MiddlewareOptions","RpcPluginOptions"],"sources":["../src/express/types.d.ts","../src/hono/types.d.ts","../src/fastify/types.d.ts","../src/koa/types.d.ts","../src/h3/types.d.ts","../src/types.d.ts","../src/index.ts"],"mappings":";;;;;;;;;;;;;;;;KAgBY,2BAA2BA;;;;;KAM3B,uBACV,UAAUC,2CAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,iBAAiBC,YACtB,MAAM,QAAQ,eAAe,iBAC1B;;;;;;;UCzBU;;EAEf,SAAS;;;;;;KAOC,oBAAoB,UAAUC,wCACxC,iBAAiB,QAAQC,oBAAkB,QACxC;;;;;;KCWO,2BAA2BC;;;;;KAM3B,uBACV,UAAUC,2CAEV,iBAAiB,QAAQ,8BACtB;;;;UAKY;;;;;;;EAOf,UACE,KAAK,gBACL,KAAK,cACL,MAAM,4BACH;;;;;;;KCtDK,uBAAuBC;;;;UAalB;;;;;;EAMf,UAAU,KAAK,SAAS,MAAM,SAAS;;;;;;KAO7B,mBAAmB,UAAUC,uCACvC,iBAAiB,QAAQ,0BACtB;;;;;;KChCO,sBAAsBC;;;;UAKjB;;;;;;EAMf,SAAS;;;;;;KAOC,kBAAkB,UAAUC,sCACtC,iBAAiB,QAAQ,yBACtB;;;;;;;UCJY;;EAEf,SAAS;;EAET,MAAM;;EAEN,SAAS;;EAET,KAAK;;EAEL,IAAI;;;;;UAMW;;EAEf,SAAS;;EAET,MAAM;;EAEN,SAAS;;EAET,KAAK;;EAEL,IAAI;;;;;;KAOM;;;;KAUA;;;;KASA;;;;KAKA;EACN;EAAiC,MAAM;;EACvC;EAA2B;;EAE7B;EACA,MAAM;;EAEJ;EAAoC,MAAM;;;;;;UAM/B;;;;;EAKf,aAAa;;;;;EAKb,cAAc;;;;;;;EAOd;;;;;EAKA;;;;;;KAOU;;;;KAIA;GAAgB,cAAc,YAAY;;;;;KAI1C,aAAa,WAAW;;;;KAIxB,YAAY,gBAAgB,YAAY;;;;;;;;;;;;;;;;;;;KAuBxC,mBAAmB;;;;;KAMnB,eACV,cAAc,YAAY,WAC1B,UAAU,cACP,QAAQ,gBAAgB,MAAM,UAAU,QAAQ;;;;;KAMzC,mBACV,cAAc,WAAW,YAAY,WACrC,UAAU,cACP,QAAQ,gBAAgB,MAAM,UAAU,QAAQ;;;;;;KAOzC,eACV,cAAc,YAAY,WAC1B,UAAU,iBACJ,MAAM;;EAEZ,MAAM,QAAQ;;EAEd,SAAS;;;;;;KAOC,4BAA4B;;EAEtC;;EAEA,UAAU;;;;;UAMK;;EAEf;;EAEA;;;;;UAMe,mBAAmB,KAAK;EACvC;EACA,SAAS,QAAQ;EACjB;EACA;;EAEA;;;;;;UAOe;;EAEf;;EAEA,SAAS;;EAET,UAAU;;EAEV;;;;;;;;UASe;;;;;;;;;;;EAWf;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;UAGe,kBACf,UAAU;;;;EAKV;;;;;;;;;;;;EAaA,gBAAgB;;;;;;;;;;EAWhB;;;;;;;;EASA;;;;;;;;EASA;;;;;EAMA;;;;;;;;;;;;;;;;;;EAmBA,UAAU,eAAe;;;;;;UAOV;;;;;EAKf;;;;;EAKA,aAAa;;;;;;;EAOb,aAAa;;;;;;KAOH,eAAe,UAAU;;EAEnC,MAAM,QAAQ;;EAEd,SAAS;;;;;;;;;;;;cC5VL,gBACJ,qBACA;EAAS;MACN,QAAQ;;;;;;;;iBA6FJ,UACP,aAAY,QAAQ,oBACnB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.mts","names":[],"sources":["../../src/express/types.d.ts","../../src/hono/types.d.ts","../../src/fastify/types.d.ts","../../src/koa/types.d.ts","../../src/h3/types.d.ts","../../src/types.d.ts","../../src/functionsMap.ts","../../src/scanForServerFiles.ts","../../src/createFunction.ts","../../src/getClientModules.ts","../../src/server-helpers.ts","../../src/context.ts","../../src/options.ts"],"mappings":";;;;;;;;;;;;;;;;UA+BiB;;;;;;;EAOf,UACE,KAAK,kBAAkB,SACvB,KAAK,
|
|
1
|
+
{"version":3,"file":"server.d.mts","names":["Response"],"sources":["../../src/express/types.d.ts","../../src/hono/types.d.ts","../../src/fastify/types.d.ts","../../src/koa/types.d.ts","../../src/h3/types.d.ts","../../src/types.d.ts","../../src/functionsMap.ts","../../src/scanForServerFiles.ts","../../src/createFunction.ts","../../src/getClientModules.ts","../../src/server-helpers.ts","../../src/context.ts","../../src/options.ts"],"mappings":"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"}
|
package/llms.txt
CHANGED
|
@@ -10,7 +10,7 @@ Vite plugin for automatic RPC generation from server functions. One module (`src
|
|
|
10
10
|
- **Multi-prefix**: `rpcPrefix` (default `"__rpc"` via the exported `defaultPrefix` constant) registers the function in a prefix-scoped map (`getFunctionsForPrefix(prefix)`). Same names can coexist under different prefixes (versioned/namespaced APIs); middleware dispatches to the prefix-scoped map; the plugin generates client stubs per prefix. See `wiki/multi-prefix-guide.md`
|
|
11
11
|
- `RPCError(message, code?, data?)` — throwable typed error, exported from `@thednp/rpc/server` (as is `formatError`); throw for server-side failures (not validation — return `{ error }` for expected user-facing problems). Dev 500 body: `{ error, code, data }`; prod: generic `{ error: "Internal Server Error" }` only. Client rejection `message` = the error string
|
|
12
12
|
- `provideRequestContext(init, cb)` / `getRequestContext()` — per-request `AsyncLocalStorage` context available to all server function code; `RequestEvent` includes `nativeEvent`, `locals`, adapter-bound `redirect`; works across Express/Fastify/Hono/Koa/h3
|
|
13
|
-
- `unwrapEnvelope<T>(json)` — client helper from `@thednp/rpc/helpers` to unwrap `{ data }` wire protocol
|
|
13
|
+
- `unwrapEnvelope<T>(json)` — client helper from `@thednp/rpc/helpers` to unwrap the `{ data }` wire protocol envelope; for native HTTP clients (Deno, Bun, curl-equivalents) that don't use the auto-generated fetch stubs. Same error contract as `handleResponse`: a **top-level** `error` (emitted only for 400/404/405/415/500) throws; `{ data: { error } }` resolves normally (validation-as-data). Status-code agnostic — keep the `res.ok` check. `RPCError` is server-side only (`@thednp/rpc/server`), not a client export, and its `code`/`data` are stripped in production regardless
|
|
14
14
|
- Registered function names become URL paths: `POST /{prefix}/{name}` with body `JSON.stringify(args)`
|
|
15
15
|
|
|
16
16
|
## Wire Protocol (client module → server)
|
|
@@ -20,7 +20,7 @@ Vite plugin for automatic RPC generation from server functions. One module (`src
|
|
|
20
20
|
- **multipart/form-data** POST: client sends the `FormData` passed as the first argument (browser sets the boundary). Server-side, parse before the handler runs — framework multipart middleware BEFORE the RPC middleware (multer, @fastify/multipart, koa-body, hono formData) and the parsed fields object becomes the function argument; without a parser the handler gets `{ raw: "<body>" }`, which you must parse yourself (busboy or formidable — Node has no built-in multipart parser)
|
|
21
21
|
- **GET:** `/{prefix}/{fnName}?args=encodeURIComponent(JSON.stringify(args))`
|
|
22
22
|
- **Response:** always `{ data: <result> }` — 200 wraps everything, even `{ error }` responses (they resolve as data, not thrown)
|
|
23
|
-
- **Status codes:** 200 (success or validation-as-data), 404/405/415/500 (transport errors → data promise rejects). 415 = request Content-Type doesn't match the function's declared contentType (json/text strict,
|
|
23
|
+
- **Status codes:** 200 (success or validation-as-data), 400/404/405/415/500 (transport errors → data promise rejects). 400 = GET `?args=` present but not a JSON array. 415 = request Content-Type doesn't match the function's declared contentType (json/text strict; form functions accept either form encoding, but a JSON-declared function does **not** accept form bodies — leniency is one-directional; headerless requests exempt). 500 body: generic `{ error: "Internal Server Error" }` in production; dev includes the message (and `code`/`data` for `RPCError`)
|
|
24
24
|
- See `wiki/wire-protocol.md` for full curl examples
|
|
25
25
|
|
|
26
26
|
## Config
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thednp/rpc",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"license": "MIT",
|
|
3
|
+
"version": "0.3.4",
|
|
4
|
+
"license": "MIT",
|
|
5
5
|
"author": "thednp",
|
|
6
6
|
"description": "⚡ A Vite plugin for creating server functions with automatic Remote Procedure Calls (RPC)",
|
|
7
7
|
"homepage": "https://github.com/thednp/rpc#readme",
|
|
@@ -85,24 +85,24 @@
|
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
87
|
"express": "^5.2.1",
|
|
88
|
-
"fastify": "^5.12.
|
|
88
|
+
"fastify": "^5.12.5",
|
|
89
89
|
"fastify-plugin": "^6.0.0",
|
|
90
|
-
"h3": "2.0.1-rc.
|
|
91
|
-
"hono": "^4.13.
|
|
90
|
+
"h3": "2.0.1-rc.32",
|
|
91
|
+
"hono": "^4.13.9",
|
|
92
92
|
"koa": "^3.2.1",
|
|
93
|
-
"vite": "^8.3.
|
|
93
|
+
"vite": "^8.3.1"
|
|
94
94
|
},
|
|
95
95
|
"devDependencies": {
|
|
96
96
|
"@hono/node-server": "^2.1.1",
|
|
97
97
|
"@types/express": "^5.0.6",
|
|
98
98
|
"@types/koa": "^3.0.3",
|
|
99
|
-
"@types/node": "^26.
|
|
100
|
-
"@vitest/coverage-istanbul": "^5.0.
|
|
101
|
-
"@vitest/ui": "^5.0.
|
|
99
|
+
"@types/node": "^26.6.3",
|
|
100
|
+
"@vitest/coverage-istanbul": "^5.0.2",
|
|
101
|
+
"@vitest/ui": "^5.0.2",
|
|
102
102
|
"tsdown": "^0.23.0",
|
|
103
103
|
"typescript": "^7.0.2",
|
|
104
104
|
"vite-plugin-strip-comments": "^0.0.10",
|
|
105
|
-
"vitest": "^5.0.
|
|
105
|
+
"vitest": "^5.0.2"
|
|
106
106
|
},
|
|
107
107
|
"packageManager": "pnpm@11.22.0"
|
|
108
108
|
}
|