devframe 0.9.8 → 0.9.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/dist/_shared-CCzf9Hub.d.mts +83 -0
  2. package/dist/_shared-U8D-xzrp.mjs +115 -0
  3. package/dist/adapters/build.d.mts +6 -6
  4. package/dist/adapters/build.mjs +33 -36
  5. package/dist/adapters/cac.d.mts +12 -1
  6. package/dist/adapters/cac.mjs +8 -8
  7. package/dist/adapters/dev.d.mts +17 -17
  8. package/dist/adapters/dev.mjs +2 -2
  9. package/dist/adapters/embedded.d.mts +1 -1
  10. package/dist/adapters/initiate.d.mts +31 -28
  11. package/dist/adapters/initiate.mjs +1 -1
  12. package/dist/adapters/mcp.d.mts +28 -14
  13. package/dist/adapters/mcp.mjs +76 -40
  14. package/dist/{agent-tool-name-EgfoFO8C.mjs → agent-tool-name-DGzeuWcO.mjs} +2 -2
  15. package/dist/cli/main.d.mts +1 -1
  16. package/dist/cli/main.mjs +41 -17
  17. package/dist/client/index.d.mts +36 -36
  18. package/dist/client/index.mjs +90 -52
  19. package/dist/{client-assets-C7SXgTWY.mjs → client-assets-DNdhagGm.mjs} +2 -2
  20. package/dist/constants.d.mts +8 -7
  21. package/dist/constants.mjs +7 -6
  22. package/dist/{context-omlma_1n.mjs → context-B29Oh0oA.mjs} +166 -93
  23. package/dist/{context-DftYyXaR.d.mts → context-BJiKJqyW.d.mts} +5 -5
  24. package/dist/{context-CLF4-8Pp.d.mts → context-CHW4BZOj.d.mts} +203 -134
  25. package/dist/{context-KUqAfawQ.mjs → context-jXO5vzjH.mjs} +3 -3
  26. package/dist/{crypto-token-XCqTSMg9.mjs → crypto-token-TVbGGFuj.mjs} +1 -1
  27. package/dist/{dev-CzkPCJwi.mjs → dev-CRYHx7iG.mjs} +73 -40
  28. package/dist/{diagnostics-BXWW3VK-.mjs → diagnostics-DZA6KQ_Z.mjs} +32 -19
  29. package/dist/{events-4daw0uK9.mjs → events-BV4Dj59a.mjs} +2 -2
  30. package/dist/{services-install-CqqQVls5.mjs → host-agent-CKqSTEX9.mjs} +15 -151
  31. package/dist/{host-h3-fRbF9yor.mjs → host-h3-BROI6SQ4.mjs} +2 -2
  32. package/dist/in-page-channel/index.d.mts +14 -14
  33. package/dist/in-page-channel/index.mjs +63 -54
  34. package/dist/{index-D3k0BIhe.d.mts → index-CNC390b_.d.mts} +4 -4
  35. package/dist/{index-BzyopqxV.d.mts → index-pIVt92jP.d.mts} +9 -9
  36. package/dist/index.d.mts +8 -8
  37. package/dist/index.mjs +2 -2
  38. package/dist/{instance-registry-BjlMQbFB.mjs → instance-registry-DCq762YJ.mjs} +9 -9
  39. package/dist/{instance-shell-Tcp5hGoq.d.mts → instance-shell-DZipaP9k.d.mts} +18 -18
  40. package/dist/{instance-shell-CpVW4zG0.mjs → instance-shell-DvYCwrDy.mjs} +223 -67
  41. package/dist/internal/index.d.mts +45 -26
  42. package/dist/internal/index.mjs +10 -10
  43. package/dist/node/auth.d.mts +2 -2
  44. package/dist/node/auth.mjs +2 -2
  45. package/dist/node/hub-internals.d.mts +2 -2
  46. package/dist/node/hub-internals.mjs +2 -2
  47. package/dist/node/index.d.mts +5 -5
  48. package/dist/node/index.mjs +2 -2
  49. package/dist/{nostics-SPGDgHEP.d.mts → nostics-0sKe5HIj.d.mts} +2 -2
  50. package/dist/{nostics-CzECRXpE.mjs → nostics-D0PvLZsn.mjs} +1 -1
  51. package/dist/origin-DGDH8BI9.d.mts +45 -0
  52. package/dist/origin-DZit9g29.mjs +80 -0
  53. package/dist/recipes/common-rpc-functions.d.mts +5 -5
  54. package/dist/recipes/common-rpc-functions.mjs +3 -3
  55. package/dist/recipes/interactive-auth.d.mts +6 -6
  56. package/dist/recipes/interactive-auth.mjs +6 -6
  57. package/dist/{remote-assets-DezQmPQU.mjs → remote-assets-DMFzLSoH.mjs} +27 -7
  58. package/dist/{remote-assets-B13_vRkJ.d.mts → remote-assets-eBvSKZMS.d.mts} +7 -7
  59. package/dist/{revoke-COLW0cOi.mjs → revoke-cqztiCX4.mjs} +1 -1
  60. package/dist/rpc/dump.d.mts +19 -12
  61. package/dist/rpc/dump.mjs +2 -2
  62. package/dist/rpc/index.d.mts +2 -2
  63. package/dist/rpc/index.mjs +2 -3
  64. package/dist/rpc/transports/sse-client.d.mts +1 -1
  65. package/dist/rpc/transports/sse-client.mjs +30 -14
  66. package/dist/rpc/transports/sse-server.d.mts +4 -4
  67. package/dist/rpc/transports/sse-server.mjs +7 -7
  68. package/dist/rpc/transports/ws-bun.d.mts +5 -5
  69. package/dist/rpc/transports/ws-bun.mjs +8 -2
  70. package/dist/rpc/transports/ws-client.d.mts +1 -1
  71. package/dist/rpc/transports/ws-client.mjs +2 -2
  72. package/dist/rpc/transports/ws-deno.d.mts +4 -4
  73. package/dist/rpc/transports/ws-deno.mjs +8 -2
  74. package/dist/rpc/transports/ws-server.d.mts +2 -1
  75. package/dist/rpc/transports/ws-server.mjs +232 -1
  76. package/dist/{rpc-core-DlHd276T.mjs → rpc-core-CvkU23bQ.mjs} +14 -2
  77. package/dist/{rpc-core-B3W3z9ff.d.mts → rpc-core-DsyDViQy.d.mts} +4 -4
  78. package/dist/serialization-DFg5sUt6.mjs +116 -0
  79. package/dist/services-install-Ben4yh52.mjs +154 -0
  80. package/dist/session-DPfj6zQG.mjs +16 -0
  81. package/dist/{shared-state--sduc6Xi.mjs → shared-state-CaxB1q0h.mjs} +2 -2
  82. package/dist/{simple-schema-DQPZrAaZ.mjs → simple-schema-Bi40l-P6.mjs} +2 -2
  83. package/dist/{sse-client-DwX021ST.d.mts → sse-client-BQ4jS3eO.d.mts} +4 -4
  84. package/dist/{state-CK9LjrnT.mjs → state-Mwt6--el.mjs} +17 -9
  85. package/dist/{storage-BXNVhKyR.mjs → storage-B70AXK64.mjs} +2 -2
  86. package/dist/types/index.d.mts +6 -6
  87. package/dist/{types-CMdPYhOx.d.mts → types-Dq4i156u.d.mts} +17 -17
  88. package/dist/utils/agent-tool-name.d.mts +2 -2
  89. package/dist/utils/agent-tool-name.mjs +2 -2
  90. package/dist/utils/colors.d.mts +1 -1
  91. package/dist/utils/crypto-token.d.mts +1 -1
  92. package/dist/utils/crypto-token.mjs +1 -1
  93. package/dist/utils/events.d.mts +1 -1
  94. package/dist/utils/nostics.d.mts +1 -1
  95. package/dist/utils/nostics.mjs +1 -1
  96. package/dist/utils/open.mjs +1 -1
  97. package/dist/utils/origin.d.mts +2 -0
  98. package/dist/utils/origin.mjs +80 -0
  99. package/dist/utils/remote-assets.d.mts +2 -2
  100. package/dist/utils/remote-assets.mjs +1 -1
  101. package/dist/utils/serve-static.d.mts +4 -4
  102. package/dist/utils/serve-static.mjs +56 -19
  103. package/dist/utils/shared-state.d.mts +1 -1
  104. package/dist/utils/shared-state.mjs +1 -1
  105. package/dist/utils/simple-schema.d.mts +5 -5
  106. package/dist/utils/simple-schema.mjs +2 -2
  107. package/dist/utils/streaming-channel.d.mts +1 -1
  108. package/dist/utils/streaming-channel.mjs +2 -2
  109. package/dist/utils/when.d.mts +5 -5
  110. package/dist/{validation-YrZH6atx.mjs → validation-CfV1s0SF.mjs} +2 -2
  111. package/dist/{wire-codec-0K-o5MYW.mjs → wire-codec-BHGt0sSs.mjs} +3 -3
  112. package/dist/{static-DV_qUSRS.mjs → write-BuLCrn9h.mjs} +153 -137
  113. package/dist/{ws-client-7_lZi-B9.d.mts → ws-client-Xpxt0oPA.d.mts} +3 -3
  114. package/dist/{ws-server-VyHlanzy.d.mts → ws-server-D2nv68Hg.d.mts} +13 -37
  115. package/package.json +3 -6
  116. package/skills/devframe/SKILL.md +5 -3
  117. package/dist/_shared-BM3PdYli.mjs +0 -68
  118. package/dist/_shared-mtXpcJIv.d.mts +0 -41
  119. package/dist/diagnostics-CD8nlgll.mjs +0 -39
  120. package/dist/serialization-BGzEwAdr.mjs +0 -77
  121. package/dist/ws-server-Y_AumUQH.mjs +0 -294
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "devframe",
3
3
  "type": "module",
4
- "version": "0.9.8",
4
+ "version": "0.9.10",
5
5
  "description": "Framework for building one portable devtool integration that runs in any viewer.",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -56,6 +56,7 @@
56
56
  "./utils/nanoid": "./dist/utils/nanoid.mjs",
57
57
  "./utils/nostics": "./dist/utils/nostics.mjs",
58
58
  "./utils/open": "./dist/utils/open.mjs",
59
+ "./utils/origin": "./dist/utils/origin.mjs",
59
60
  "./utils/remote-assets": "./dist/utils/remote-assets.mjs",
60
61
  "./utils/simple-schema": "./dist/utils/simple-schema.mjs",
61
62
  "./utils/serve-static": "./dist/utils/serve-static.mjs",
@@ -76,21 +77,18 @@
76
77
  ],
77
78
  "peerDependencies": {
78
79
  "@modelcontextprotocol/client": "^2.0.0",
79
- "@modelcontextprotocol/server": "^2.0.0",
80
80
  "cac": "^7.0.0"
81
81
  },
82
82
  "peerDependenciesMeta": {
83
83
  "@modelcontextprotocol/client": {
84
84
  "optional": true
85
85
  },
86
- "@modelcontextprotocol/server": {
87
- "optional": true
88
- },
89
86
  "cac": {
90
87
  "optional": true
91
88
  }
92
89
  },
93
90
  "dependencies": {
91
+ "@modelcontextprotocol/server": "^2.0.0",
94
92
  "@standard-schema/spec": "^1.1.0",
95
93
  "birpc": "^4.2.0",
96
94
  "crossws": "^0.4.12",
@@ -102,7 +100,6 @@
102
100
  },
103
101
  "devDependencies": {
104
102
  "@modelcontextprotocol/client": "^2.0.0",
105
- "@modelcontextprotocol/server": "^2.0.0",
106
103
  "cac": "^7.0.0",
107
104
  "get-port-please": "^3.2.0",
108
105
  "immer": "^11.1.18",
@@ -405,7 +405,9 @@ defineRpcFunction({
405
405
  })
406
406
  ```
407
407
 
408
- Or register tools / resources directly on `ctx.agent.registerTool({ id, description, safety, handler })` and `ctx.agent.registerResource({ id, name, mimeType, read })`. Expose the API over MCP:
408
+ Or register tools / resources directly on `ctx.agent.registerTool({ id, description, safety, handler })` and `ctx.agent.registerResource({ id, name, mimeType, read })`.
409
+
410
+ The dev server serves this surface over HTTP automatically: the `mcp: 'auto'` default mounts the Streamable-HTTP route at `<base>__mcp` once the agent surface is non-empty (`mcp: true` forces on, `mcp: false` off). For stdio:
409
411
 
410
412
  ```ts
411
413
  import { createMcpServer } from 'devframe/adapters/mcp'
@@ -413,7 +415,7 @@ import { createMcpServer } from 'devframe/adapters/mcp'
413
415
  await createMcpServer(myDevframe, { transport: 'stdio' })
414
416
  ```
415
417
 
416
- `@modelcontextprotocol/server` is a peer dependency. The CLI adapter also exposes `my-tool mcp` (route node-side logs to stderr - stdout is the transport). Safety classifications (`'read' | 'action' | 'destructive'`) drive MCP hint annotations that coding agents use to prompt for confirmation. In a hub, `ctx.commands` entries opt into the same agent-facing API with an `agent` field and reach MCP through the aggregate endpoint.
418
+ The CLI adapter also exposes `my-tool mcp` (route node-side logs to stderr - stdout is the transport). Safety classifications (`'read' | 'action' | 'destructive'`) drive MCP hint annotations that coding agents use to prompt for confirmation. In a hub, `ctx.commands` entries opt into the same agent-facing API with an `agent` field and reach MCP through the aggregate endpoint.
417
419
 
418
420
  ## Author SPA
419
421
 
@@ -508,7 +510,7 @@ Plus broadcasts (`devframe:docks:activate`, `devframe:terminals:updated`, `devfr
508
510
 
509
511
  The hub is headless; `DevframeHubUi` is pure data (`viewer` / `embedded` / `assets` / `setup`). `@devframes/hub-ui`'s `createUi()` is the reference hub UI provider - a standalone devtools page plus a floating dock injected via one `<script type="module" src="/__devframes/embedded.js">` tag. It takes `branding`, `dockPreferences`, and `embeddedVisibility` (`'normal'` / `'passive'` / `'hidden'`). Another hub UI provider supplies a different object to the same slot and reuses all the infrastructure. Renderer modules for opt-in dock types compose at the hub via `initHub({ renderers })`.
510
512
 
511
- Two copyable reference hubs mount every built-in devframe behind an icon dock rail - the shape Vite DevTools wears, shrunk to the smallest thing you can build your own hub UI provider from: [`examples/hub-vite/`](https://github.com/devframes/devframe/tree/main/examples/hub-vite) (~120-line Vite host, vanilla DOM UI) and [`examples/hub-next/`](https://github.com/devframes/devframe/tree/main/examples/hub-next) (Next.js App Router). The `hub-*-minimal` family shows the default `createUi()` mount across Vite, Next, Nitro, Hono, Fastify, SvelteKit, Deno, and Rsbuild.
513
+ Two copyable reference hubs mount every built-in devframe behind an icon dock rail - the shape Vite DevTools wears, shrunk to the smallest thing you can build your own hub UI provider from: [`examples/custom-hub-vite/`](https://github.com/devframes/devframe/tree/main/examples/custom-hub-vite) (~120-line Vite host, vanilla DOM UI) and [`examples/custom-hub-next/`](https://github.com/devframes/devframe/tree/main/examples/custom-hub-next) (Next.js App Router). The `hub-*` family (`hub-vite`, `hub-next`, `hub-deno`, `hub-fastify`, `hub-hono`, `hub-nitro`, `hub-rsbuild`, `hub-sveltekit`) shows the default `createUi()` mount across Vite, Next, Nitro, Hono, Fastify, SvelteKit, Deno, and Rsbuild.
512
514
 
513
515
  ## Framework kits: two scopes
514
516
 
@@ -1,68 +0,0 @@
1
- import "./constants.mjs";
2
- import { n as getPort } from "./dist-CZXfGEkd.mjs";
3
- import { cleanDoubleSlashes, withLeadingSlash, withTrailingSlash, withoutLeadingSlash } from "ufo";
4
- //#region src/adapters/_shared.ts
5
- const DEFAULT_PORT = 9999;
6
- /**
7
- * Resolve the mount base path for a devframe's SPA. Hosted adapters
8
- * (`vite`, `embedded`) default to `/__<id>/` so they don't collide
9
- * with the host app; standalone adapters (`cli`, `build`)
10
- * default to `/` because they own the origin.
11
- *
12
- * The devframe author can override with `basePath` on the definition.
13
- */
14
- function resolveBasePath(def, kind) {
15
- if (def.basePath) return normalizeBasePath(def.basePath);
16
- return kind === "standalone" ? "/" : `/__${def.id}/`;
17
- }
18
- function normalizeBasePath(base) {
19
- return cleanDoubleSlashes(withTrailingSlash(withLeadingSlash(base)));
20
- }
21
- /**
22
- * Resolve the listening port for `createDevServer` (and `createHandler`'s
23
- * side-car tiers), honoring the definition's `cli.port` / `cli.portRange` /
24
- * `cli.random` settings. Exposed separately so authors who run their own
25
- * argv parsing can resolve a port up-front (to print it, log it, etc.)
26
- * before starting the server.
27
- */
28
- async function resolveDevServerPort(def, options = {}) {
29
- const host = options.host ?? def.cli?.host ?? "localhost";
30
- const portOptions = {
31
- port: options.defaultPort ?? def.cli?.port ?? DEFAULT_PORT,
32
- host
33
- };
34
- if (def.cli?.portRange) portOptions.portRange = def.cli.portRange;
35
- if (def.cli?.random) portOptions.random = def.cli.random;
36
- return getPort(portOptions);
37
- }
38
- /**
39
- * Normalize the `cli.mcp` / `mcp` option (`boolean | McpRouteOptions`) into
40
- * concrete options, or `undefined` when the MCP route is disabled.
41
- */
42
- function resolveMcpConfig(mcp) {
43
- if (!mcp) return void 0;
44
- return mcp === true ? {} : mcp;
45
- }
46
- /**
47
- * Resolve the `mcp` entry a `__connection.json` should advertise for a dev
48
- * server started with the given `mcp` option (falling back to `def.cli?.mcp`,
49
- * exactly like `createDevServer`), or `undefined` when the route is
50
- * disabled.
51
- *
52
- * Hosted bridges that hand-roll their connection meta pass the side-car
53
- * `port`: the advertised path becomes absolute (the side-car mounts at `/`)
54
- * and the client dials `<page-host>:<port><path>`. Without `port` the path
55
- * stays relative, resolved against `__connection.json`'s own location (the
56
- * same-server default).
57
- */
58
- function resolveMcpConnectionMeta(def, mcp, port) {
59
- const config = resolveMcpConfig(mcp ?? def.cli?.mcp);
60
- if (!config) return void 0;
61
- const route = withoutLeadingSlash(config.path ?? "__mcp");
62
- return port != null ? {
63
- path: withLeadingSlash(route),
64
- port
65
- } : { path: route };
66
- }
67
- //#endregion
68
- export { resolveMcpConnectionMeta as i, resolveBasePath as n, resolveDevServerPort as r, normalizeBasePath as t };
@@ -1,41 +0,0 @@
1
- import { F as DevframeDefinition, G as McpRouteOptions, I as DevframeDeploymentKind, t as ConnectionMeta } from "./context-CLF4-8Pp.mjs";
2
- //#region src/adapters/_shared.d.ts
3
- /**
4
- * Resolve the mount base path for a devframe's SPA. Hosted adapters
5
- * (`vite`, `embedded`) default to `/__<id>/` so they don't collide
6
- * with the host app; standalone adapters (`cli`, `build`)
7
- * default to `/` because they own the origin.
8
- *
9
- * The devframe author can override with `basePath` on the definition.
10
- */
11
- declare function resolveBasePath(def: DevframeDefinition, kind: DevframeDeploymentKind): string;
12
- declare function normalizeBasePath(base: string): string;
13
- interface ResolveDevServerPortOptions {
14
- /** Bind host (passed to `get-port-please` for in-use detection). */
15
- host?: string;
16
- /** Override the preferred port. Default: `def.cli?.port ?? 9999`. */
17
- defaultPort?: number;
18
- }
19
- /**
20
- * Resolve the listening port for `createDevServer` (and `createHandler`'s
21
- * side-car tiers), honoring the definition's `cli.port` / `cli.portRange` /
22
- * `cli.random` settings. Exposed separately so authors who run their own
23
- * argv parsing can resolve a port up-front (to print it, log it, etc.)
24
- * before starting the server.
25
- */
26
- declare function resolveDevServerPort(def: DevframeDefinition, options?: ResolveDevServerPortOptions): Promise<number>;
27
- /**
28
- * Resolve the `mcp` entry a `__connection.json` should advertise for a dev
29
- * server started with the given `mcp` option (falling back to `def.cli?.mcp`,
30
- * exactly like `createDevServer`), or `undefined` when the route is
31
- * disabled.
32
- *
33
- * Hosted bridges that hand-roll their connection meta pass the side-car
34
- * `port`: the advertised path becomes absolute (the side-car mounts at `/`)
35
- * and the client dials `<page-host>:<port><path>`. Without `port` the path
36
- * stays relative, resolved against `__connection.json`'s own location (the
37
- * same-server default).
38
- */
39
- declare function resolveMcpConnectionMeta(def: DevframeDefinition, mcp: boolean | McpRouteOptions | undefined, port?: number): ConnectionMeta['mcp'];
40
- //#endregion
41
- export { resolveMcpConnectionMeta as a, resolveDevServerPort as i, normalizeBasePath as n, resolveBasePath as r, ResolveDevServerPortOptions as t };
@@ -1,39 +0,0 @@
1
- import { i as defineDiagnostics } from "./nostics-CzECRXpE.mjs";
2
- //#region src/rpc/diagnostics.ts
3
- const diagnostics = defineDiagnostics({
4
- docsBase: "https://devfra.me/errors",
5
- codes: {
6
- DF0019: {
7
- why: (p) => `RPC function "${p.name}" has \`agent\` set but \`jsonSerializable\` is not \`true\` — MCP requires JSON-serializable data.`,
8
- fix: "Set `jsonSerializable: true` if the payload is JSON-safe, or remove `agent` to keep it RPC-only."
9
- },
10
- DF0020: {
11
- why: (p) => `RPC function "${p.name}" declares \`jsonSerializable: true\` but the value at "${p.path}" is a ${p.type}.`,
12
- fix: "Either drop `jsonSerializable: true` (falls back to structured-clone) or change the value to a JSON-safe shape."
13
- },
14
- DF0021: {
15
- why: (p) => `RPC function "${p.name}" is already registered`,
16
- fix: "Use the `force` parameter to overwrite an existing registration."
17
- },
18
- DF0022: { why: (p) => `RPC function "${p.name}" is not registered. Use register() to add new functions.` },
19
- DF0023: { why: (p) => `RPC function "${p.name}" is not registered` },
20
- DF0024: { why: (p) => `Either handler or setup function must be provided for RPC function "${p.name}"` },
21
- DF0025: { why: (p) => `Function "${p.name}" not found in dump store` },
22
- DF0026: { why: (p) => `No dump match for "${p.name}" with args: ${p.args}` },
23
- DF0027: { why: (p) => `Function "${p.name}" with type "${p.type}" cannot have dump configuration. Only "static" and "query" types support dumps.` },
24
- DF0028: {
25
- why: (p) => `Function "${p.name}" with type "${p.type}" cannot use \`snapshot: true\`. Only "query" functions support this sugar; "static" functions have equivalent default behavior already.`,
26
- fix: "Remove `snapshot: true`, or change the function type to `query`."
27
- },
28
- DF0043: {
29
- why: (p) => `RPC function "${p.name}" received an invalid argument at position ${p.index}: ${p.issues}`,
30
- fix: "Pass a value that satisfies the `args` schema declared for this function."
31
- },
32
- DF0044: {
33
- why: (p) => `RPC function "${p.name}" returned a value that failed its \`returns\` schema: ${p.issues}`,
34
- fix: "Make the handler return a value that satisfies the `returns` schema, or relax the schema."
35
- }
36
- }
37
- });
38
- //#endregion
39
- export { diagnostics as t };
@@ -1,77 +0,0 @@
1
- import { t as diagnostics } from "./diagnostics-CD8nlgll.mjs";
2
- //#region src/rpc/serialization.ts
3
- /**
4
- * Wire format used by the live RPC transports (WebSocket and SSE).
5
- *
6
- * - **JSON (default, unprefixed):** payload is plain JSON text. Used when
7
- * the dispatched method is declared `jsonSerializable: true`. Encoded
8
- * via {@link strictJsonStringify} (rejects non-JSON values), decoded
9
- * via `JSON.parse`.
10
- * - **Structured-clone (`s:` prefix):** payload is `s:` followed by
11
- * `structured-clone-es` text. Used when the method is declared
12
- * `jsonSerializable: false` (or omitted, the default). Round-trips
13
- * `Map`, `Set`, `Date`, `BigInt`, cycles, and class instances.
14
- *
15
- * birpc envelopes always start with `{`, so a leading byte that is not
16
- * `s` is unambiguously JSON. Each direction independently chooses its
17
- * encoding from local definitions — request and response are not
18
- * coupled by a mirror rule.
19
- */
20
- const STRUCTURED_CLONE_PREFIX = "s:";
21
- /**
22
- * `JSON.stringify` with a single-pass strict replacer.
23
- *
24
- * Throws `DF0020` synchronously when the value contains a type JSON
25
- * cannot round-trip losslessly: `Map`, `Set`, `Date`, `BigInt`, class
26
- * instances, or `undefined` inside an array (silently becomes `null`).
27
- *
28
- * Native pass-throughs (no extra work needed):
29
- * - circular references — `JSON.stringify` raises `TypeError`.
30
- * - `BigInt` at top level — caught here for a friendlier error path.
31
- *
32
- * Lenient cases (allowed without throwing):
33
- * - `undefined` as an object property — legitimate optional field;
34
- * JSON.stringify just omits it.
35
- * - `undefined` at the root — legitimate "action returned nothing".
36
- * - `Symbol` / `Function` values — semantically "drop me" in JSON.
37
- *
38
- * `fnName` is used only for the diagnostic message — pass the RPC
39
- * function name when calling from a wire serializer / dump writer so
40
- * the error points at the offending function.
41
- */
42
- function strictJsonStringify(value, fnName = "") {
43
- return JSON.stringify(value, function strictReplacer(key, val) {
44
- const holder = this;
45
- const original = holder != null ? holder[key] : val;
46
- if (original === void 0) {
47
- if (Array.isArray(holder)) throw nonJsonAt(fnName, "undefined", holder, key);
48
- return val;
49
- }
50
- if (original === null) return val;
51
- if (typeof original === "bigint") throw nonJsonAt(fnName, "BigInt", holder, key);
52
- if (typeof original === "object") {
53
- if (original instanceof Map) throw nonJsonAt(fnName, "Map", holder, key);
54
- if (original instanceof Set) throw nonJsonAt(fnName, "Set", holder, key);
55
- if (original instanceof Date) throw nonJsonAt(fnName, "Date", holder, key);
56
- if (Array.isArray(original)) return val;
57
- const proto = Object.getPrototypeOf(original);
58
- if (proto !== null && proto !== Object.prototype) throw nonJsonAt(fnName, original.constructor?.name ?? "class instance", holder, key);
59
- }
60
- return val;
61
- });
62
- }
63
- function nonJsonAt(fnName, type, parent, key) {
64
- const path = formatPath(parent, key);
65
- return diagnostics.DF0020({
66
- name: fnName || "<anonymous>",
67
- type,
68
- path
69
- });
70
- }
71
- function formatPath(parent, key) {
72
- if (Array.isArray(parent)) return `[${key}]`;
73
- if (key === "") return "<root>";
74
- return key;
75
- }
76
- //#endregion
77
- export { strictJsonStringify as n, STRUCTURED_CLONE_PREFIX as t };
@@ -1,294 +0,0 @@
1
- import "./constants.mjs";
2
- import { t as createRpcWireCodec } from "./wire-codec-0K-o5MYW.mjs";
3
- import { n as randomToken, r as timingSafeEqual } from "./crypto-token-XCqTSMg9.mjs";
4
- import { createServer } from "node:http";
5
- import { createServer as createServer$1 } from "node:https";
6
- import crossws from "crossws/adapters/node";
7
- //#region src/rpc/transports/session.ts
8
- let sessionId = 0;
9
- /**
10
- * Mint the per-connection session meta every transport binding shares —
11
- * one id space across transports, so session bookkeeping (streaming
12
- * subscriptions, shared-state sync, auth trust) never collides between a
13
- * WS peer and an SSE session on the same server.
14
- */
15
- function createRpcSessionMeta() {
16
- return {
17
- id: sessionId++,
18
- subscribedStates: /* @__PURE__ */ new Set()
19
- };
20
- }
21
- //#endregion
22
- //#region src/rpc/transports/ws-server.ts
23
- /**
24
- * Create a live, token-protected origin allowlist for external browser
25
- * viewers. Pass it to {@link WsRpcTransportOptions.allowedOrigins}, then use
26
- * `registerFromUrl()` in the connection metadata handler to authorize a
27
- * viewer without sharing a mutable array or disabling DNS-rebinding protection.
28
- */
29
- function createWsOriginRegistry(options = {}) {
30
- const token = randomToken();
31
- const origins = new Set(options.allowedOrigins ?? []);
32
- function normalizeOrigin(origin) {
33
- if (!origin) return;
34
- try {
35
- const url = new URL(origin);
36
- const normalized = url.origin === "null" ? `${url.protocol}//${url.host}` : url.origin;
37
- return origin === normalized ? normalized : void 0;
38
- } catch {}
39
- }
40
- function registerOrigin(origin, candidateToken) {
41
- const normalized = normalizeOrigin(origin);
42
- if (!normalized || !candidateToken || !timingSafeEqual(token, candidateToken)) return false;
43
- if (options.validateOrigin && !options.validateOrigin(normalized)) return false;
44
- origins.add(normalized);
45
- return true;
46
- }
47
- return {
48
- token,
49
- registerFromUrl(url) {
50
- let parsed;
51
- try {
52
- parsed = new URL(url, "http://localhost");
53
- } catch {
54
- return;
55
- }
56
- const origin = parsed.searchParams.get("devframe_viewer_origin") ?? void 0;
57
- return registerOrigin(origin, parsed.searchParams.get("devframe_viewer_origin_token") ?? void 0) ? origin : void 0;
58
- },
59
- isAllowed(origin) {
60
- return isAllowedOrigin(origin, [...origins]);
61
- }
62
- };
63
- }
64
- const EMPTY_DEFS = /* @__PURE__ */ new Map();
65
- function NOOP() {}
66
- function listen(server, port, host) {
67
- return new Promise((resolve, reject) => {
68
- const onError = (error) => reject(error);
69
- server.once("error", onError);
70
- try {
71
- server.listen(port, host, () => {
72
- server.off("error", onError);
73
- resolve();
74
- });
75
- } catch (error) {
76
- server.off("error", onError);
77
- reject(error);
78
- }
79
- });
80
- }
81
- /** Compare two URL paths ignoring a trailing slash. */
82
- function pathMatches(a, b) {
83
- const strip = (p) => p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
84
- return strip(a) === strip(b);
85
- }
86
- /**
87
- * Whether `hostname` names a loopback host: `localhost` (or any `*.localhost`
88
- * subdomain), the IPv6 loopback `::1`, or an IPv4 literal inside the
89
- * `127.0.0.0/8` loopback block.
90
- *
91
- * The IPv4 case is matched **structurally** — the whole hostname must be a
92
- * canonical dotted-decimal IPv4 literal whose first octet is `127`. A bare
93
- * `startsWith('127.')` prefix check would also accept an attacker-controlled
94
- * DNS name that merely *begins* with `127.` (`127.attacker.example`,
95
- * `127.0.0.1.attacker.example`), letting a cross-origin browser page defeat
96
- * the loopback origin gate that guards the RPC/MCP surface (a DNS-rebinding /
97
- * cross-site WebSocket-hijacking bypass). Requiring a real IPv4 literal keeps
98
- * genuine loopback addresses (`127.0.0.1`, `127.5.5.5`) allowed while rejecting
99
- * those DNS names.
100
- */
101
- function isLoopbackHostname(hostname) {
102
- const h = hostname.replace(/^\[|\]$/g, "");
103
- if (h === "localhost" || h.endsWith(".localhost") || h === "::1") return true;
104
- return isLoopbackIPv4(h);
105
- }
106
- /** A canonical dotted-decimal IPv4 literal in `127.0.0.0/8`. */
107
- function isLoopbackIPv4(hostname) {
108
- const octets = hostname.split(".");
109
- if (octets.length !== 4 || !octets.every(isDecimalOctet)) return false;
110
- return Number(octets[0]) === 127;
111
- }
112
- /** A single canonical IPv4 octet: 1–3 digits, no leading zero, value 0–255. */
113
- function isDecimalOctet(part) {
114
- if (!/^\d{1,3}$/.test(part) || part.length > 1 && part[0] === "0") return false;
115
- return Number(part) <= 255;
116
- }
117
- /**
118
- * Default origin policy for a localhost dev tool: allow requests with no
119
- * `Origin` header (native, non-browser clients), allow any loopback origin
120
- * (so cross-port localhost dev setups keep working), and allow explicitly
121
- * configured origins. Everything else — a real remote page in the dev's
122
- * browser — is rejected.
123
- */
124
- function isAllowedOrigin(origin, allowedOrigins) {
125
- if (!origin) return true;
126
- if (allowedOrigins.includes(origin)) return true;
127
- try {
128
- return isLoopbackHostname(new URL(origin).hostname);
129
- } catch {
130
- return false;
131
- }
132
- }
133
- function isWsOriginRegistry(value) {
134
- return !!value && !Array.isArray(value);
135
- }
136
- /**
137
- * Build the `upgrade` listener that hands a request to the crossws adapter,
138
- * optionally filtered to a single `path`. Non-matching requests are left
139
- * untouched so other upgrade listeners (e.g. a Vite dev server's HMR socket)
140
- * can claim them, unless `destroyUnmatched` is set.
141
- */
142
- function createUpgradeListener(ws, path, destroyUnmatched, allowedOrigins) {
143
- return (req, socket, head) => {
144
- socket.on("error", () => {});
145
- if (path) {
146
- let pathname = req.url ?? "/";
147
- try {
148
- pathname = new URL(req.url ?? "/", "http://localhost").pathname;
149
- } catch {}
150
- if (!pathMatches(pathname, path)) {
151
- if (destroyUnmatched) {
152
- socket.write("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
153
- socket.destroy();
154
- }
155
- return;
156
- }
157
- }
158
- const originAllowed = isWsOriginRegistry(allowedOrigins) ? allowedOrigins.isAllowed(req.headers.origin) : isAllowedOrigin(req.headers.origin, allowedOrigins || []);
159
- if (allowedOrigins !== false && !originAllowed) {
160
- socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
161
- socket.destroy();
162
- return;
163
- }
164
- ws.handleUpgrade(req, socket, head);
165
- };
166
- }
167
- /**
168
- * The per-peer lifecycle hooks driving a devframe RPC WebSocket, shaped for
169
- * any [crossws](https://crossws.h3.dev) adapter. {@link attachWsRpcTransport}
170
- * feeds them to the Node adapter; runtime-specific attachments (e.g. Bun's
171
- * fetch-upgrade adapter) reuse the same hooks so every transport speaks the
172
- * identical wire protocol — one birpc channel per peer, per-method
173
- * `jsonSerializable` dispatch between strict JSON and structured-clone.
174
- */
175
- function createWsRpcPeerHooks(rpcGroup, options = {}) {
176
- const { onConnected = NOOP, onDisconnected = NOOP, definitions = EMPTY_DEFS, serialize: serializeOverride, deserialize: deserializeOverride } = options;
177
- const states = /* @__PURE__ */ new WeakMap();
178
- return {
179
- open: (peer) => {
180
- const meta = createRpcSessionMeta();
181
- meta.peer = peer;
182
- const connection = {
183
- id: meta.id,
184
- transport: "websocket",
185
- request: peer.request,
186
- send: (data) => peer.send(data),
187
- close: (code, reason) => peer.close(code, reason),
188
- peer
189
- };
190
- const codec = createRpcWireCodec(definitions);
191
- const state = {
192
- meta,
193
- connection,
194
- channel: void 0
195
- };
196
- const channel = {
197
- post: (data) => {
198
- peer.send(data);
199
- },
200
- on: (fn) => {
201
- state.onMessage = fn;
202
- },
203
- serialize: serializeOverride ?? codec.serialize,
204
- deserialize: deserializeOverride ?? codec.deserialize,
205
- meta
206
- };
207
- state.channel = channel;
208
- states.set(peer, state);
209
- rpcGroup.updateChannels((channels) => {
210
- channels.push(channel);
211
- });
212
- onConnected(connection, meta);
213
- },
214
- message: (peer, message) => {
215
- states.get(peer)?.onMessage?.(message.text());
216
- },
217
- close: (peer) => {
218
- const state = states.get(peer);
219
- if (!state) return;
220
- states.delete(peer);
221
- rpcGroup.updateChannels((channels) => {
222
- const index = channels.indexOf(state.channel);
223
- if (index >= 0) channels.splice(index, 1);
224
- });
225
- onDisconnected(state.connection, state.meta);
226
- }
227
- };
228
- }
229
- /**
230
- * Attach a WebSocket transport to an existing RPC group, powered by
231
- * [crossws](https://crossws.h3.dev). Either attach to an existing HTTP(S)
232
- * `server` (sharing its port, optionally scoped to a `path`), or let this
233
- * helper create a standalone server from `port` / `host` / `https`.
234
- *
235
- * Returns the crossws node adapter, standalone-server readiness/address
236
- * accessors, `detach` (remove the upgrade listener from a shared `server`),
237
- * and `close` (full deterministic teardown).
238
- */
239
- function attachWsRpcTransport(rpcGroup, options = {}) {
240
- const { server, port, host = "localhost", path, destroyUnmatched = false, unbound, https, allowedOrigins } = options;
241
- const ws = crossws({ hooks: createWsRpcPeerHooks(rpcGroup, options) });
242
- const sharedUpgradeListener = createUpgradeListener(ws, path, destroyUnmatched, allowedOrigins);
243
- const ownedUpgradeListener = createUpgradeListener(ws, path, true, allowedOrigins);
244
- /** Bind a server's `upgrade` events, tracked so `close()` detaches them. */
245
- const attachments = /* @__PURE__ */ new Set();
246
- function attachTo(target, listener) {
247
- target.on("upgrade", listener);
248
- const detachOne = () => {
249
- target.off("upgrade", listener);
250
- attachments.delete(detachOne);
251
- };
252
- attachments.add(detachOne);
253
- return detachOne;
254
- }
255
- let ready = Promise.resolve();
256
- let ownedServer;
257
- if (unbound) {} else if (server) attachTo(server, sharedUpgradeListener);
258
- else if (https) {
259
- ownedServer = createServer$1(https);
260
- attachTo(ownedServer, ownedUpgradeListener);
261
- ready = listen(ownedServer, port ?? 0, host);
262
- } else {
263
- ownedServer = createServer((_req, res) => {
264
- res.writeHead(426, { "content-type": "text/plain" });
265
- res.end("Upgrade Required");
266
- });
267
- attachTo(ownedServer, ownedUpgradeListener);
268
- ready = listen(ownedServer, port ?? 0, host);
269
- }
270
- const activeServer = server ?? ownedServer;
271
- function detachAll() {
272
- for (const detachOne of [...attachments]) detachOne();
273
- }
274
- return {
275
- ws,
276
- ready,
277
- address: () => activeServer?.address() ?? null,
278
- handleUpgrade: sharedUpgradeListener,
279
- attach: (target) => attachTo(target, sharedUpgradeListener),
280
- detach: detachAll,
281
- async close() {
282
- detachAll();
283
- ws.closeAll(void 0, void 0, true);
284
- if (ownedServer) {
285
- const srv = ownedServer;
286
- await ready.catch(() => {});
287
- if (!srv.listening) return;
288
- await new Promise((r) => srv.close(() => r()));
289
- }
290
- }
291
- };
292
- }
293
- //#endregion
294
- export { isLoopbackHostname as a, isAllowedOrigin as i, createWsOriginRegistry as n, createRpcSessionMeta as o, createWsRpcPeerHooks as r, attachWsRpcTransport as t };