devframe 0.9.8 → 0.9.9

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 (116) hide show
  1. package/dist/{_shared-BM3PdYli.mjs → _shared-C08SdQRl.mjs} +15 -4
  2. package/dist/{_shared-mtXpcJIv.d.mts → _shared-CK3czkH4.d.mts} +25 -2
  3. package/dist/adapters/build.d.mts +6 -6
  4. package/dist/adapters/build.mjs +32 -22
  5. package/dist/adapters/cac.d.mts +16 -1
  6. package/dist/adapters/cac.mjs +8 -7
  7. package/dist/adapters/dev.d.mts +13 -13
  8. package/dist/adapters/dev.mjs +2 -2
  9. package/dist/adapters/embedded.d.mts +1 -1
  10. package/dist/adapters/initiate.d.mts +28 -26
  11. package/dist/adapters/initiate.mjs +1 -1
  12. package/dist/adapters/mcp.d.mts +28 -14
  13. package/dist/adapters/mcp.mjs +75 -39
  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 +83 -51
  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-CLF4-8Pp.d.mts → context-4AIVRLYF.d.mts} +158 -122
  23. package/dist/{context-omlma_1n.mjs → context-CwPUinjn.mjs} +126 -88
  24. package/dist/{context-KUqAfawQ.mjs → context-DIUvJW4D.mjs} +3 -3
  25. package/dist/{context-DftYyXaR.d.mts → context-rgBSPFHS.d.mts} +5 -5
  26. package/dist/{crypto-token-XCqTSMg9.mjs → crypto-token-TVbGGFuj.mjs} +1 -1
  27. package/dist/{dev-CzkPCJwi.mjs → dev-Eqq5PX0k.mjs} +37 -18
  28. package/dist/{diagnostics-BXWW3VK-.mjs → diagnostics-BMfN8mb1.mjs} +30 -17
  29. package/dist/{diagnostics-CD8nlgll.mjs → diagnostics-D0EVm5M6.mjs} +2 -2
  30. package/dist/{events-4daw0uK9.mjs → events-BV4Dj59a.mjs} +2 -2
  31. package/dist/{host-h3-fRbF9yor.mjs → host-h3-BJ4siyz7.mjs} +1 -1
  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-BzyopqxV.d.mts → index-pIVt92jP.d.mts} +9 -9
  35. package/dist/{index-D3k0BIhe.d.mts → index-pKl67H6w.d.mts} +4 -4
  36. package/dist/index.d.mts +8 -8
  37. package/dist/index.mjs +2 -2
  38. package/dist/{instance-registry-BjlMQbFB.mjs → instance-registry-C7-41RSp.mjs} +8 -8
  39. package/dist/{instance-shell-Tcp5hGoq.d.mts → instance-shell-7Z52PE4_.d.mts} +18 -18
  40. package/dist/{instance-shell-CpVW4zG0.mjs → instance-shell-cdVETpfz.mjs} +227 -58
  41. package/dist/internal/index.d.mts +40 -23
  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-BIFWOlBW.mjs} +26 -6
  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 +2 -11
  61. package/dist/rpc/dump.mjs +1 -1
  62. package/dist/rpc/index.d.mts +2 -2
  63. package/dist/rpc/index.mjs +3 -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-B3W3z9ff.d.mts → rpc-core-Ceu6AUz8.d.mts} +4 -4
  77. package/dist/{rpc-core-DlHd276T.mjs → rpc-core-DVQ7xi89.mjs} +14 -2
  78. package/dist/{serialization-BGzEwAdr.mjs → serialization-DTZnPQ1-.mjs} +20 -18
  79. package/dist/{services-install-CqqQVls5.mjs → services-install-P_DV7VYL.mjs} +32 -21
  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-CeQQWzM-.mjs} +4 -4
  85. package/dist/{static-DV_qUSRS.mjs → static-DZ8P2Kn7.mjs} +130 -135
  86. package/dist/{storage-BXNVhKyR.mjs → storage-Dccf_IyD.mjs} +1 -1
  87. package/dist/types/index.d.mts +6 -6
  88. package/dist/{types-CMdPYhOx.d.mts → types-Dq4i156u.d.mts} +17 -17
  89. package/dist/utils/agent-tool-name.d.mts +2 -2
  90. package/dist/utils/agent-tool-name.mjs +2 -2
  91. package/dist/utils/colors.d.mts +1 -1
  92. package/dist/utils/crypto-token.d.mts +1 -1
  93. package/dist/utils/crypto-token.mjs +1 -1
  94. package/dist/utils/events.d.mts +1 -1
  95. package/dist/utils/nostics.d.mts +1 -1
  96. package/dist/utils/nostics.mjs +1 -1
  97. package/dist/utils/open.mjs +1 -1
  98. package/dist/utils/origin.d.mts +2 -0
  99. package/dist/utils/origin.mjs +80 -0
  100. package/dist/utils/remote-assets.d.mts +2 -2
  101. package/dist/utils/remote-assets.mjs +1 -1
  102. package/dist/utils/serve-static.d.mts +4 -4
  103. package/dist/utils/serve-static.mjs +56 -19
  104. package/dist/utils/shared-state.d.mts +1 -1
  105. package/dist/utils/shared-state.mjs +1 -1
  106. package/dist/utils/simple-schema.d.mts +5 -5
  107. package/dist/utils/simple-schema.mjs +2 -2
  108. package/dist/utils/streaming-channel.d.mts +1 -1
  109. package/dist/utils/streaming-channel.mjs +2 -2
  110. package/dist/utils/when.d.mts +5 -5
  111. package/dist/{validation-YrZH6atx.mjs → validation-BOy8IoFZ.mjs} +2 -2
  112. package/dist/{wire-codec-0K-o5MYW.mjs → wire-codec-dcCeXcSo.mjs} +3 -3
  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 +2 -1
  116. package/dist/ws-server-Y_AumUQH.mjs +0 -294
@@ -1,6 +1,6 @@
1
- import { d as DevframeRpcConnection, n as WsOriginRegistry, u as DevframeNodeRpcSessionMeta } from "./ws-server-VyHlanzy.mjs";
2
- import { Ct as DevframeAuthHandler, U as DevframeSseOptions, W as DevframeWsOptions, _t as DevframeRpcServerFunctions, gt as DevframeRpcClientFunctions, o as DevframeNodeContext, s as DevframeNodeRpcSession, t as ConnectionMeta } from "./context-CLF4-8Pp.mjs";
3
- import "./index-D3k0BIhe.mjs";
1
+ import { c as DevframeNodeRpcSessionMeta, l as DevframeRpcConnection, n as WsOriginRegistry } from "./ws-server-D2nv68Hg.mjs";
2
+ import { U as DevframeSseOptions, W as DevframeWsOptions, _t as DevframeRpcClientFunctions, o as DevframeNodeContext, s as DevframeNodeRpcSession, t as ConnectionMeta, vt as DevframeRpcServerFunctions, wt as DevframeAuthHandler } from "./context-4AIVRLYF.mjs";
3
+ import "./index-pKl67H6w.mjs";
4
4
  import { BirpcGroup } from "birpc";
5
5
  import { NodeAdapter } from "crossws/adapters/node";
6
6
  import { Buffer } from "node:buffer";
@@ -10,7 +10,7 @@ import { H3 } from "h3";
10
10
  //#region src/node/instance-registry.d.ts
11
11
  /**
12
12
  * One running devframe instance, as recorded in the instance registry.
13
- * Records are self-describing JSON — additive fields are safe.
13
+ * Records are self-describing JSON, so additive fields are safe.
14
14
  */
15
15
  interface DevframeInstanceRecord {
16
16
  /** Process id of the dev server. */
@@ -58,7 +58,7 @@ interface DevframeInstanceRegistration {
58
58
  * The record is written atomically to `<dir>/<pid>-<port>.json` and removed
59
59
  * by {@link DevframeInstanceRegistration.unregister}. Records surviving a
60
60
  * crash are pruned by readers whose liveness probe fails. Registration never
61
- * throws — a write failure degrades to a coded warning (`DF0045`), since a
61
+ * throws; a write failure degrades to a coded warning (`DF0045`), since a
62
62
  * dev server must not die over discovery metadata.
63
63
  */
64
64
  declare function registerDevframeInstance(record: DevframeInstanceRecord, options?: {
@@ -85,7 +85,7 @@ declare function listLiveDevframeInstances(options?: {
85
85
  //#endregion
86
86
  //#region src/node/instance-shell.d.ts
87
87
  /**
88
- * The live handle for a bound HTTP + WebSocket RPC server — what the
88
+ * The live handle for a bound HTTP + WebSocket RPC server: what the
89
89
  * side-car / shared-server tiers produce and what {@link createDevServer}
90
90
  * re-exposes through its own return contract.
91
91
  */
@@ -101,7 +101,7 @@ interface StartedServer {
101
101
  ws?: NodeAdapter;
102
102
  rpcGroup: BirpcGroup<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>;
103
103
  /**
104
- * The {@link ConnectionMeta} descriptor for this server — the same shape a
104
+ * The {@link ConnectionMeta} descriptor for this server, the same shape a
105
105
  * `__connection.json` route should serve so a devframe client's
106
106
  * `resolveWsUrl` can dial back in.
107
107
  */
@@ -111,15 +111,15 @@ interface StartedServer {
111
111
  /**
112
112
  * How the instance's RPC socket is bound:
113
113
  *
114
- * - `sidecar` — its own HTTP+WS server on a dedicated port (`ws.port` /
114
+ * - `sidecar`: its own HTTP+WS server on a dedicated port (`ws.port` /
115
115
  * `ws.sidecar`), advertised with that port.
116
- * - `server` — a shared upgrade route on the host's `node:http` server.
117
- * - `external` — no local transport: `ws.url` alone names a server that owns
116
+ * - `server`: a shared upgrade route on the host's `node:http` server.
117
+ * - `external`: no local transport; `ws.url` alone names a server that owns
118
118
  * both the socket and its auth.
119
- * - `unbound` — the transport exists but nothing is bound to it yet; the host
119
+ * - `unbound`: the transport exists but nothing is bound to it yet; the host
120
120
  * drives it through {@link InstanceShell.handleUpgrade} /
121
121
  * {@link InstanceShell.attach}.
122
- * - `disabled` — `ws: false`: no WebSocket at all; clients connect over the
122
+ * - `disabled` (`ws: false`): no WebSocket at all; clients connect over the
123
123
  * SSE endpoint instead (`backend: 'sse'`).
124
124
  */
125
125
  type InstanceWsTier = 'sidecar' | 'server' | 'external' | 'unbound' | 'disabled';
@@ -154,9 +154,9 @@ interface CreateInstanceShellOptions<TContext extends DevframeNodeContext> {
154
154
  auth?: boolean | DevframeAuthHandler;
155
155
  /** Host `node:http` server to share the WS upgrade with. */
156
156
  server?: Server;
157
- /** Explicit WebSocket control — see {@link DevframeWsOptions}. `false` disables the socket (SSE-only). */
157
+ /** Explicit WebSocket control; see {@link DevframeWsOptions}. `false` disables the socket (SSE-only). */
158
158
  ws?: DevframeWsOptions | false;
159
- /** SSE endpoint control — enabled by default; `false` disables, an object renames the route. */
159
+ /** SSE endpoint control, enabled by default; `false` disables, an object renames the route. */
160
160
  sse?: boolean | DevframeSseOptions;
161
161
  /** Bind host for a side-car WebSocket server. Default: `localhost`. */
162
162
  host?: string;
@@ -177,7 +177,7 @@ interface CreateInstanceShellOptions<TContext extends DevframeNodeContext> {
177
177
  resolveSidecarPort?: (host: string) => Promise<number>;
178
178
  /**
179
179
  * Publish this instance in the global registry (`~/.devframe/instances/`)
180
- * once its public origin is known — a dynamic import so the registry code
180
+ * once its public origin is known, via a dynamic import so the registry code
181
181
  * stays out of instances that opt out. Omit to skip registration.
182
182
  */
183
183
  register?: InstanceRegisterConfig;
@@ -190,7 +190,7 @@ interface CreateInstanceShellOptions<TContext extends DevframeNodeContext> {
190
190
  }
191
191
  /**
192
192
  * The identity a shell needs to publish itself in the global instance
193
- * registry — the parts it can't derive on its own. The shell fills in
193
+ * registry: the parts it can't derive on its own. The shell fills in
194
194
  * `pid` / `origin` / `port` / `basePath` / `mcp` / `startedAt` once the
195
195
  * origin resolves, then merges {@link InstanceRegisterConfig.overrides} last.
196
196
  */
@@ -240,8 +240,8 @@ declare function samePath(a: string, b: string): boolean;
240
240
  * The shared machinery behind `initDevframe` and `initHub`: one mount base,
241
241
  * one h3 app, one lazily-derived public origin (and the auth banner that waits
242
242
  * for it), one WebSocket binding, and the fetch / connect-middleware pair that
243
- * serves them. Each factory supplies only what makes it itself — its context,
244
- * its routes, its diagnostics — through `init` / `mount`.
243
+ * serves them. Each factory supplies only what makes it itself (its context,
244
+ * its routes, its diagnostics) through `init` / `mount`.
245
245
  *
246
246
  * Nothing here listens on a port unless a side-car was explicitly requested:
247
247
  * the default tier leaves the socket `unbound`, so a host chains it onto its
@@ -1,7 +1,8 @@
1
1
  import "./constants.mjs";
2
- import { t as diagnostics } from "./diagnostics-BXWW3VK-.mjs";
3
- import { r as importServicePackage } from "./services-install-CqqQVls5.mjs";
4
- import { t as getInternalContext } from "./context-KUqAfawQ.mjs";
2
+ import { r as validateOriginCandidate } from "./origin-DZit9g29.mjs";
3
+ import { t as diagnostics } from "./diagnostics-BMfN8mb1.mjs";
4
+ import { r as importServicePackage } from "./services-install-P_DV7VYL.mjs";
5
+ import { t as getInternalContext } from "./context-DIUvJW4D.mjs";
5
6
  import { createInteractiveAuth } from "./recipes/interactive-auth.mjs";
6
7
  import { createServer } from "node:http";
7
8
  import process from "node:process";
@@ -20,6 +21,21 @@ async function importRuntimeModule(specifier) {
20
21
  return await importServicePackage(specifier, [import.meta.url]);
21
22
  }
22
23
  //#endregion
24
+ //#region src/node/runtime.ts
25
+ /**
26
+ * Detect the current server runtime from its runtime-specific global. The
27
+ * WebSocket binding branches on this: Bun and Deno expose their sockets as
28
+ * `fetch` upgrades through `Bun.serve` / `Deno.serve` (crossws's Bun/Deno
29
+ * adapters), while the `node:http` upgrade event (and crossws's Node adapter,
30
+ * which refuses to run anywhere else) is Node-only.
31
+ */
32
+ function detectServerRuntime() {
33
+ const g = globalThis;
34
+ if (typeof g.Deno !== "undefined") return "deno";
35
+ if (typeof g.Bun !== "undefined") return "bun";
36
+ return "node";
37
+ }
38
+ //#endregion
23
39
  //#region src/node/utils.ts
24
40
  const NON_DIALABLE_HOSTS = /* @__PURE__ */ new Set([
25
41
  "0.0.0.0",
@@ -42,8 +58,17 @@ function normalizeHttpServerUrl(host, port) {
42
58
  }
43
59
  //#endregion
44
60
  //#region src/node/instance-shell.ts
61
+ function websocketConnectionMeta(rpcHost, path) {
62
+ const jsonSerializableMethods = [];
63
+ for (const def of rpcHost.definitions.values()) if (def.jsonSerializable === true) jsonSerializableMethods.push(def.name);
64
+ return {
65
+ backend: "websocket",
66
+ websocket: { path },
67
+ jsonSerializableMethods
68
+ };
69
+ }
45
70
  /**
46
- * Compose an h3 + WebSocket RPC server for a devframe context — the low-level
71
+ * Compose an h3 + WebSocket RPC server for a devframe context: the low-level
47
72
  * "listen on a port (or share one) + attach the WS transport" binding the
48
73
  * side-car and shared-server tiers below are built on. Owns and listens on a
49
74
  * fresh `node:http` server unless `server` is supplied, in which case it only
@@ -97,22 +122,13 @@ async function bindHttpAndWs(options) {
97
122
  const internal = getInternalContext(context);
98
123
  const wsUrl = `ws://${formatHostForUrl(bindHost)}:${resolvedPort}${options.path ?? ""}`;
99
124
  if (websocket) internal.setWsEndpoint({ url: wsUrl });
100
- function connectionMeta() {
101
- const jsonSerializableMethods = [];
102
- for (const def of rpcHost.definitions.values()) if (def.jsonSerializable === true) jsonSerializableMethods.push(def.name);
103
- return {
104
- backend: "websocket",
105
- websocket: { path: options.path },
106
- jsonSerializableMethods
107
- };
108
- }
109
125
  return {
110
126
  origin,
111
127
  port: resolvedPort,
112
128
  app,
113
129
  ws,
114
130
  rpcGroup: core.rpcGroup,
115
- connectionMeta,
131
+ connectionMeta: () => websocketConnectionMeta(rpcHost, options.path),
116
132
  async close() {
117
133
  await closeWs();
118
134
  if (ownsHttpServer) await new Promise((r) => httpServer.close(() => r()));
@@ -121,6 +137,105 @@ async function bindHttpAndWs(options) {
121
137
  };
122
138
  }
123
139
  /**
140
+ * Whether a request is a WebSocket upgrade aimed at `path`: the `fetch`-side
141
+ * equivalent of the Node transport's `upgrade`-event path filter, used by the
142
+ * native ({@link bindNativeHttpAndWs}) tiers to route only the RPC route to
143
+ * the socket and leave every other request to the h3 app.
144
+ */
145
+ function isWsUpgradeRequest(request, path) {
146
+ if ((request.headers.get("upgrade") ?? "").toLowerCase() !== "websocket") return false;
147
+ if (!path) return true;
148
+ try {
149
+ return samePath(new URL(request.url).pathname, path);
150
+ } catch {
151
+ return false;
152
+ }
153
+ }
154
+ /**
155
+ * The Bun/Deno counterpart to {@link bindHttpAndWs}: own and listen on a
156
+ * native `fetch`-upgrade server (`Bun.serve` / `Deno.serve`) with crossws's
157
+ * Bun/Deno adapter driving the RPC socket, since crossws's Node adapter (and
158
+ * the `node:http` `upgrade` event it needs) refuses to run off Node. Only the
159
+ * owns-a-server tiers reach here; a shared foreign `node:http` server can't be
160
+ * re-hosted on a native runtime, so that path falls back to SSE instead.
161
+ */
162
+ async function bindNativeHttpAndWs(runtime, options) {
163
+ const { context, port, core } = options;
164
+ const bindHost = options.host;
165
+ const app = new H3();
166
+ const rpcHost = context.rpc;
167
+ const wsPath = options.path;
168
+ const fail = async (closeWs, error) => {
169
+ await closeWs().catch(() => {});
170
+ throw diagnostics.DF0052({
171
+ host: bindHost,
172
+ port,
173
+ reason: error instanceof Error ? error.message : String(error),
174
+ cause: error
175
+ });
176
+ };
177
+ let resolvedPort;
178
+ let closeWs;
179
+ let closeServer;
180
+ if (runtime === "bun") {
181
+ const { attachBunWsTransport } = await import("./rpc/transports/ws-bun.mjs");
182
+ const tier = await attachBunWsTransport(core, { allowedOrigins: options.allowedOrigins });
183
+ closeWs = tier.close;
184
+ const Bun = globalThis.Bun;
185
+ let server;
186
+ try {
187
+ server = Bun.serve({
188
+ port,
189
+ hostname: bindHost,
190
+ fetch: async (request, srv) => isWsUpgradeRequest(request, wsPath) ? await tier.handleUpgrade(request, srv) : await app.fetch(request),
191
+ websocket: tier.websocket
192
+ });
193
+ } catch (error) {
194
+ return await fail(closeWs, error);
195
+ }
196
+ resolvedPort = server.port;
197
+ closeServer = async () => {
198
+ await server.stop(true);
199
+ };
200
+ } else {
201
+ const { attachDenoWsTransport } = await import("./rpc/transports/ws-deno.mjs");
202
+ const tier = await attachDenoWsTransport(core, { allowedOrigins: options.allowedOrigins });
203
+ closeWs = tier.close;
204
+ const Deno = globalThis.Deno;
205
+ let server;
206
+ try {
207
+ server = Deno.serve({
208
+ port,
209
+ hostname: bindHost,
210
+ onListen: () => {}
211
+ }, async (request, info) => isWsUpgradeRequest(request, wsPath) ? await tier.handleUpgrade(request, info) : await app.fetch(request));
212
+ } catch (error) {
213
+ return await fail(closeWs, error);
214
+ }
215
+ resolvedPort = server.addr.port;
216
+ closeServer = async () => {
217
+ await server.shutdown();
218
+ };
219
+ }
220
+ const origin = normalizeHttpServerUrl(bindHost, resolvedPort);
221
+ const internal = getInternalContext(context);
222
+ const wsUrl = `ws://${formatHostForUrl(bindHost)}:${resolvedPort}${options.path ?? ""}`;
223
+ internal.setWsEndpoint({ url: wsUrl });
224
+ return {
225
+ origin,
226
+ port: resolvedPort,
227
+ app,
228
+ ws: void 0,
229
+ rpcGroup: core.rpcGroup,
230
+ connectionMeta: () => websocketConnectionMeta(rpcHost, options.path),
231
+ async close() {
232
+ await closeServer();
233
+ await closeWs();
234
+ if (getInternalContext(context).wsEndpoint?.url === wsUrl) getInternalContext(context).setWsEndpoint(void 0);
235
+ }
236
+ };
237
+ }
238
+ /**
124
239
  * Translate the public `register?: boolean | Partial<DevframeInstanceRecord>`
125
240
  * option into a shell {@link InstanceRegisterConfig}, or `undefined` when
126
241
  * registration is opted out. The object form supplies record overrides on top
@@ -141,8 +256,8 @@ function samePath(a, b) {
141
256
  }
142
257
  /**
143
258
  * Copy a web `Response` from a fetch-style transport handler onto the h3
144
- * event's response and return its body — mirroring the MCP route's bridge.
145
- * Returning the body (a `ReadableStream`, or `''` for an empty one — h3
259
+ * event's response and return its body, mirroring the MCP route's bridge.
260
+ * Returning the body (a `ReadableStream`, or `''` for an empty one, since h3
146
261
  * middleware only falls through on `undefined`) terminates the chain with
147
262
  * the status/headers set here instead of continuing to the SPA catch-all.
148
263
  */
@@ -158,8 +273,8 @@ function respondWith(event, response) {
158
273
  * The shared machinery behind `initDevframe` and `initHub`: one mount base,
159
274
  * one h3 app, one lazily-derived public origin (and the auth banner that waits
160
275
  * for it), one WebSocket binding, and the fetch / connect-middleware pair that
161
- * serves them. Each factory supplies only what makes it itself — its context,
162
- * its routes, its diagnostics — through `init` / `mount`.
276
+ * serves them. Each factory supplies only what makes it itself (its context,
277
+ * its routes, its diagnostics) through `init` / `mount`.
163
278
  *
164
279
  * Nothing here listens on a port unless a side-car was explicitly requested:
165
280
  * the default tier leaves the socket `unbound`, so a host chains it onto its
@@ -172,6 +287,8 @@ function createInstanceShell(options) {
172
287
  const base = options.base;
173
288
  const baseNoSlash = withoutTrailingSlash(base);
174
289
  const app = options.app ?? new H3();
290
+ const runtime = detectServerRuntime();
291
+ const nativeRuntime = runtime !== "node";
175
292
  const wsDisabled = options.ws === false;
176
293
  const ws = options.ws === false ? {} : options.ws ?? {};
177
294
  const route = withoutLeadingSlash(ws.route ?? "__ws");
@@ -186,8 +303,11 @@ function createInstanceShell(options) {
186
303
  const sseRoutePath = joinURL(base, sseRoute);
187
304
  const advertisedSsePath = options.absoluteWsPath ? sseRoutePath : sseRoute;
188
305
  let derivedOrigin;
306
+ function explicitOrigin() {
307
+ return typeof options.origin === "function" ? options.origin() : options.origin;
308
+ }
189
309
  function currentOrigin() {
190
- return (typeof options.origin === "function" ? options.origin() : options.origin) || derivedOrigin;
310
+ return explicitOrigin() || derivedOrigin;
191
311
  }
192
312
  let authHandler;
193
313
  let bannerPrinted = false;
@@ -201,7 +321,7 @@ function createInstanceShell(options) {
201
321
  let registerPromise;
202
322
  /**
203
323
  * Publish the instance in the global registry the moment both its origin
204
- * and connection meta are known — at init end for a pinned origin, or on
324
+ * and connection meta are known: at init end for a pinned origin, or on
205
325
  * the first request for a derived one. Registration never throws (the
206
326
  * registry writer degrades to a coded warning), so failures never surface.
207
327
  */
@@ -210,7 +330,7 @@ function createInstanceShell(options) {
210
330
  const origin = currentOrigin();
211
331
  if (!cfg || registerPromise || !origin || !meta) return;
212
332
  const resolvedMeta = meta;
213
- registerPromise = import("./instance-registry-BjlMQbFB.mjs").then((n) => n.t).then(({ registerDevframeInstance }) => {
333
+ registerPromise = import("./instance-registry-C7-41RSp.mjs").then((n) => n.t).then(({ registerDevframeInstance }) => {
214
334
  let port = 0;
215
335
  try {
216
336
  const url = new URL(origin);
@@ -230,8 +350,20 @@ function createInstanceShell(options) {
230
350
  });
231
351
  }).catch(() => {});
232
352
  }
233
- function noteOrigin(origin) {
234
- derivedOrigin ??= origin;
353
+ /**
354
+ * Consider a request-derived origin candidate for the advertised public
355
+ * origin (which backs the OTP magic link). {@link validateOriginCandidate}
356
+ * adopts only a loopback host or an exact `allowedOrigins` match, so a raw
357
+ * inbound `Host`/URL authority never redirects the credential-bearing link.
358
+ * First-valid-origin wins: an invalid candidate leaves `derivedOrigin` unset
359
+ * (printing/registering nothing) so a later valid one can still be adopted.
360
+ */
361
+ function noteOrigin(candidate) {
362
+ if (derivedOrigin === void 0 && !explicitOrigin()) {
363
+ const allowed = options.allowedOrigins;
364
+ const accepted = validateOriginCandidate(candidate, Array.isArray(allowed) ? allowed : void 0);
365
+ if (accepted !== void 0) derivedOrigin = accepted;
366
+ }
235
367
  maybePrintBanner();
236
368
  maybeRegister();
237
369
  }
@@ -248,7 +380,7 @@ function createInstanceShell(options) {
248
380
  /**
249
381
  * Auth resolution: gate by default, `false` opts out, a handler object
250
382
  * installs a custom scheme. The `external` tier has no local transport to
251
- * gate — the server behind `ws.url` owns auth — so it resolves to nothing.
383
+ * gate (the server behind `ws.url` owns auth) so it resolves to nothing.
252
384
  */
253
385
  function resolveAuth() {
254
386
  if (options.auth === false) return false;
@@ -260,7 +392,7 @@ function createInstanceShell(options) {
260
392
  return authHandler;
261
393
  }
262
394
  /**
263
- * The context's RPC core (birpc group, session lifecycle, auth gate) —
395
+ * The context's RPC core (birpc group, session lifecycle, auth gate) is
264
396
  * one per instance, shared by every transport binding (WS and SSE), so a
265
397
  * WS peer and an SSE session live in the same session/broadcast space.
266
398
  * Built lazily: an `unbound` host that never wires a transport pays
@@ -270,7 +402,7 @@ function createInstanceShell(options) {
270
402
  let resolvedAuth = false;
271
403
  let corePromise;
272
404
  function ensureCore() {
273
- corePromise ??= import("./rpc-core-DlHd276T.mjs").then((n) => n.n).then(({ createContextRpcServer }) => createContextRpcServer({
405
+ corePromise ??= import("./rpc-core-DVQ7xi89.mjs").then((n) => n.n).then(({ createContextRpcServer }) => createContextRpcServer({
274
406
  context: ctx,
275
407
  auth: resolvedAuth,
276
408
  onPeerConnect: options.onPeerConnect,
@@ -302,14 +434,17 @@ function createInstanceShell(options) {
302
434
  */
303
435
  async function startSidecar(core) {
304
436
  const sidecarHost = options.host ?? "localhost";
305
- const start = (port) => bindHttpAndWs({
306
- context: ctx,
307
- core,
308
- host: sidecarHost,
309
- port,
310
- path: withLeadingSlash(route),
311
- allowedOrigins: options.allowedOrigins
312
- });
437
+ const start = (port) => {
438
+ const bindOptions = {
439
+ context: ctx,
440
+ core,
441
+ host: sidecarHost,
442
+ port,
443
+ path: withLeadingSlash(route),
444
+ allowedOrigins: options.allowedOrigins
445
+ };
446
+ return nativeRuntime ? bindNativeHttpAndWs(runtime, bindOptions) : bindHttpAndWs(bindOptions);
447
+ };
313
448
  if (ws.port != null) return await start(ws.port);
314
449
  const { getPort } = await import("./dist-CZXfGEkd.mjs").then((n) => n.t);
315
450
  let lastError;
@@ -326,37 +461,70 @@ function createInstanceShell(options) {
326
461
  }
327
462
  throw lastError;
328
463
  }
464
+ async function startServerTier() {
465
+ if (nativeRuntime && !ws.url) {
466
+ const core = await ensureCore();
467
+ started = {
468
+ origin: currentOrigin() ?? "",
469
+ port: 0,
470
+ app,
471
+ ws: void 0,
472
+ rpcGroup: core.rpcGroup,
473
+ connectionMeta: () => meta ?? options.onMetaUnavailable(),
474
+ close: async () => {}
475
+ };
476
+ return { wsAvailable: false };
477
+ }
478
+ started = await bindHttpAndWs({
479
+ context: ctx,
480
+ core: await ensureCore(),
481
+ host: options.host ?? "localhost",
482
+ port: 0,
483
+ server: options.server,
484
+ path: routePath,
485
+ allowedOrigins: options.allowedOrigins,
486
+ destroyUnmatched: options.destroyUnmatchedUpgrades
487
+ });
488
+ return {
489
+ websocketMeta: { path: advertisedPath },
490
+ wsAvailable: !wsDisabled
491
+ };
492
+ }
493
+ async function resolveWebsocketBinding() {
494
+ if (tier === "sidecar") {
495
+ started = await startSidecar(await ensureCore());
496
+ return {
497
+ websocketMeta: {
498
+ port: started.port,
499
+ path: route
500
+ },
501
+ wsAvailable: !wsDisabled
502
+ };
503
+ }
504
+ if (tier === "server") return startServerTier();
505
+ if (tier === "external") return {
506
+ websocketMeta: ws.url,
507
+ wsAvailable: !wsDisabled
508
+ };
509
+ return {
510
+ websocketMeta: { path: advertisedPath },
511
+ wsAvailable: !wsDisabled
512
+ };
513
+ }
329
514
  async function init() {
330
515
  const result = await options.init(api);
331
516
  ctx = result.context;
332
517
  dispose = result.dispose;
333
518
  resolvedAuth = tier === "external" ? false : resolveAuth();
334
- let websocketMeta;
335
- if (tier === "sidecar") {
336
- started = await startSidecar(await ensureCore());
337
- websocketMeta = {
338
- port: started.port,
339
- path: route
340
- };
341
- } else if (tier === "server") {
342
- started = await bindHttpAndWs({
343
- context: ctx,
344
- core: await ensureCore(),
345
- host: options.host ?? "localhost",
346
- port: 0,
347
- server: options.server,
348
- path: routePath,
349
- allowedOrigins: options.allowedOrigins,
350
- destroyUnmatched: options.destroyUnmatchedUpgrades
351
- });
352
- websocketMeta = { path: advertisedPath };
353
- } else if (tier === "external") websocketMeta = ws.url;
354
- else if (tier === "unbound") websocketMeta = { path: advertisedPath };
519
+ const binding = await resolveWebsocketBinding();
520
+ const wsAvailable = binding.wsAvailable;
521
+ let websocketMeta = binding.websocketMeta;
355
522
  if (!wsDisabled && ws.url) websocketMeta = ws.url;
356
523
  if (sseEnabled) app.use(sseRoutePath, defineHandler(async (event) => respondWith(event, await (await ensureSse()).handler(event.req))));
524
+ if (!wsDisabled && !wsAvailable && !sseEnabled) diagnostics.DF0075({ runtime }, { method: "warn" });
357
525
  meta = {
358
- backend: wsDisabled ? sseEnabled ? "sse" : "none" : "websocket",
359
- ...websocketMeta !== void 0 ? { websocket: websocketMeta } : {},
526
+ backend: !wsDisabled && wsAvailable ? "websocket" : sseEnabled ? "sse" : "none",
527
+ ...wsAvailable && websocketMeta !== void 0 ? { websocket: websocketMeta } : {},
360
528
  ...sseEnabled ? { sse: { path: advertisedSsePath } } : {},
361
529
  ...result.mcp ? { mcp: result.mcp } : {}
362
530
  };
@@ -371,7 +539,7 @@ function createInstanceShell(options) {
371
539
  contextPromise.catch(() => {});
372
540
  /**
373
541
  * The `unbound` tier: the RPC core and its crossws adapter, bound to
374
- * nothing. Built on the first `attach` / `handleUpgrade` — a host that
542
+ * nothing. Built on the first `attach` / `handleUpgrade`; a host that
375
543
  * never wires the socket (or whose runtime brings its own WS transport)
376
544
  * pays nothing for it, not even the adapter's imports.
377
545
  */
@@ -437,6 +605,7 @@ function createInstanceShell(options) {
437
605
  if (tier === "disabled") throw diagnostics.DF0057();
438
606
  if (tier === "external") throw diagnostics.DF0056({ url: ws.url });
439
607
  if (tier !== "unbound") throw diagnostics.DF0055({ tier });
608
+ if (nativeRuntime) throw diagnostics.DF0076({ runtime });
440
609
  }
441
610
  /**
442
611
  * Publish the socket's absolute URL on the context, so surfaces that hand