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
@@ -1,2 +1,233 @@
1
- import { a as isLoopbackHostname, i as isAllowedOrigin, n as createWsOriginRegistry, r as createWsRpcPeerHooks, t as attachWsRpcTransport } from "../../ws-server-Y_AumUQH.mjs";
1
+ import "../../constants.mjs";
2
+ import { t as createRpcWireCodec } from "../../wire-codec-BHGt0sSs.mjs";
3
+ import { n as isLoopbackHostname, t as isAllowedOrigin } from "../../origin-DZit9g29.mjs";
4
+ import { t as createRpcSessionMeta } from "../../session-DPfj6zQG.mjs";
5
+ import { n as randomToken, r as timingSafeEqual } from "../../crypto-token-TVbGGFuj.mjs";
6
+ import { createServer } from "node:http";
7
+ import { createServer as createServer$1 } from "node:https";
8
+ import crossws from "crossws/adapters/node";
9
+ //#region src/rpc/transports/ws-server.ts
10
+ /**
11
+ * Create a live, token-protected origin allowlist for external browser
12
+ * viewers. Pass it to {@link WsRpcTransportOptions.allowedOrigins}, then use
13
+ * `registerFromUrl()` in the connection metadata handler to authorize a
14
+ * viewer without sharing a mutable array or disabling DNS-rebinding protection.
15
+ */
16
+ function createWsOriginRegistry(options = {}) {
17
+ const token = randomToken();
18
+ const origins = new Set(options.allowedOrigins ?? []);
19
+ function normalizeOrigin(origin) {
20
+ if (!origin) return;
21
+ try {
22
+ const url = new URL(origin);
23
+ const normalized = url.origin === "null" ? `${url.protocol}//${url.host}` : url.origin;
24
+ return origin === normalized ? normalized : void 0;
25
+ } catch {}
26
+ }
27
+ function registerOrigin(origin, candidateToken) {
28
+ const normalized = normalizeOrigin(origin);
29
+ if (!normalized || !candidateToken || !timingSafeEqual(token, candidateToken)) return false;
30
+ if (options.validateOrigin && !options.validateOrigin(normalized)) return false;
31
+ origins.add(normalized);
32
+ return true;
33
+ }
34
+ return {
35
+ token,
36
+ registerFromUrl(url) {
37
+ let parsed;
38
+ try {
39
+ parsed = new URL(url, "http://localhost");
40
+ } catch {
41
+ return;
42
+ }
43
+ const origin = parsed.searchParams.get("devframe_viewer_origin") ?? void 0;
44
+ return registerOrigin(origin, parsed.searchParams.get("devframe_viewer_origin_token") ?? void 0) ? origin : void 0;
45
+ },
46
+ isAllowed(origin) {
47
+ return isAllowedOrigin(origin, [...origins]);
48
+ }
49
+ };
50
+ }
51
+ const EMPTY_DEFS = /* @__PURE__ */ new Map();
52
+ function NOOP() {}
53
+ function listen(server, port, host) {
54
+ return new Promise((resolve, reject) => {
55
+ const onError = (error) => reject(error);
56
+ server.once("error", onError);
57
+ try {
58
+ server.listen(port, host, () => {
59
+ server.off("error", onError);
60
+ resolve();
61
+ });
62
+ } catch (error) {
63
+ server.off("error", onError);
64
+ reject(error);
65
+ }
66
+ });
67
+ }
68
+ /** Compare two URL paths ignoring a trailing slash. */
69
+ function pathMatches(a, b) {
70
+ const strip = (p) => p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
71
+ return strip(a) === strip(b);
72
+ }
73
+ function isWsOriginRegistry(value) {
74
+ return !!value && !Array.isArray(value);
75
+ }
76
+ /**
77
+ * Build the `upgrade` listener that hands a request to the crossws adapter,
78
+ * optionally filtered to a single `path`. Non-matching requests are left
79
+ * untouched so other upgrade listeners (e.g. a Vite dev server's HMR socket)
80
+ * can claim them, unless `destroyUnmatched` is set.
81
+ */
82
+ function createUpgradeListener(ws, path, destroyUnmatched, allowedOrigins) {
83
+ return (req, socket, head) => {
84
+ socket.on("error", () => {});
85
+ if (path) {
86
+ let pathname = req.url ?? "/";
87
+ try {
88
+ pathname = new URL(req.url ?? "/", "http://localhost").pathname;
89
+ } catch {}
90
+ if (!pathMatches(pathname, path)) {
91
+ if (destroyUnmatched) {
92
+ socket.write("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
93
+ socket.destroy();
94
+ }
95
+ return;
96
+ }
97
+ }
98
+ const originAllowed = isWsOriginRegistry(allowedOrigins) ? allowedOrigins.isAllowed(req.headers.origin) : isAllowedOrigin(req.headers.origin, allowedOrigins || []);
99
+ if (allowedOrigins !== false && !originAllowed) {
100
+ socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
101
+ socket.destroy();
102
+ return;
103
+ }
104
+ ws.handleUpgrade(req, socket, head);
105
+ };
106
+ }
107
+ /**
108
+ * The per-peer lifecycle hooks driving a devframe RPC WebSocket, shaped for
109
+ * any [crossws](https://crossws.h3.dev) adapter. {@link attachWsRpcTransport}
110
+ * feeds them to the Node adapter; runtime-specific attachments (e.g. Bun's
111
+ * fetch-upgrade adapter) reuse the same hooks so every transport speaks the
112
+ * identical wire protocol: one birpc channel per peer, per-method
113
+ * `jsonSerializable` dispatch between strict JSON and structured-clone.
114
+ */
115
+ function createWsRpcPeerHooks(rpcGroup, options = {}) {
116
+ const { onConnected = NOOP, onDisconnected = NOOP, definitions = EMPTY_DEFS, serialize: serializeOverride, deserialize: deserializeOverride } = options;
117
+ const states = /* @__PURE__ */ new WeakMap();
118
+ return {
119
+ open: (peer) => {
120
+ const meta = createRpcSessionMeta();
121
+ meta.peer = peer;
122
+ const connection = {
123
+ id: meta.id,
124
+ transport: "websocket",
125
+ request: peer.request,
126
+ send: (data) => peer.send(data),
127
+ close: (code, reason) => peer.close(code, reason),
128
+ peer
129
+ };
130
+ const codec = createRpcWireCodec(definitions);
131
+ const state = {
132
+ meta,
133
+ connection
134
+ };
135
+ const channel = {
136
+ post: (data) => {
137
+ peer.send(data);
138
+ },
139
+ on: (fn) => {
140
+ state.onMessage = fn;
141
+ },
142
+ serialize: serializeOverride ?? codec.serialize,
143
+ deserialize: deserializeOverride ?? codec.deserialize,
144
+ meta
145
+ };
146
+ state.channel = channel;
147
+ states.set(peer, state);
148
+ rpcGroup.updateChannels((channels) => {
149
+ channels.push(channel);
150
+ });
151
+ onConnected(connection, meta);
152
+ },
153
+ message: (peer, message) => {
154
+ states.get(peer)?.onMessage?.(message.text());
155
+ },
156
+ close: (peer) => {
157
+ const state = states.get(peer);
158
+ if (!state) return;
159
+ states.delete(peer);
160
+ rpcGroup.updateChannels((channels) => {
161
+ const index = state.channel ? channels.indexOf(state.channel) : -1;
162
+ if (index >= 0) channels.splice(index, 1);
163
+ });
164
+ onDisconnected(state.connection, state.meta);
165
+ }
166
+ };
167
+ }
168
+ /**
169
+ * Attach a WebSocket transport to an existing RPC group, powered by
170
+ * [crossws](https://crossws.h3.dev). Either attach to an existing HTTP(S)
171
+ * `server` (sharing its port, optionally scoped to a `path`), or let this
172
+ * helper create a standalone server from `port` / `host` / `https`.
173
+ *
174
+ * Returns the crossws node adapter, standalone-server readiness/address
175
+ * accessors, `detach` (remove the upgrade listener from a shared `server`),
176
+ * and `close` (full deterministic teardown).
177
+ */
178
+ function attachWsRpcTransport(rpcGroup, options = {}) {
179
+ const { server, port, host = "localhost", path, destroyUnmatched = false, unbound, https, allowedOrigins } = options;
180
+ const ws = crossws({ hooks: createWsRpcPeerHooks(rpcGroup, options) });
181
+ const sharedUpgradeListener = createUpgradeListener(ws, path, destroyUnmatched, allowedOrigins);
182
+ const ownedUpgradeListener = createUpgradeListener(ws, path, true, allowedOrigins);
183
+ /** Bind a server's `upgrade` events, tracked so `close()` detaches them. */
184
+ const attachments = /* @__PURE__ */ new Set();
185
+ function attachTo(target, listener) {
186
+ target.on("upgrade", listener);
187
+ const detachOne = () => {
188
+ target.off("upgrade", listener);
189
+ attachments.delete(detachOne);
190
+ };
191
+ attachments.add(detachOne);
192
+ return detachOne;
193
+ }
194
+ let ready = Promise.resolve();
195
+ let ownedServer;
196
+ if (unbound) {} else if (server) attachTo(server, sharedUpgradeListener);
197
+ else if (https) {
198
+ ownedServer = createServer$1(https);
199
+ attachTo(ownedServer, ownedUpgradeListener);
200
+ ready = listen(ownedServer, port ?? 0, host);
201
+ } else {
202
+ ownedServer = createServer((_req, res) => {
203
+ res.writeHead(426, { "content-type": "text/plain" });
204
+ res.end("Upgrade Required");
205
+ });
206
+ attachTo(ownedServer, ownedUpgradeListener);
207
+ ready = listen(ownedServer, port ?? 0, host);
208
+ }
209
+ const activeServer = server ?? ownedServer;
210
+ function detachAll() {
211
+ for (const detachOne of [...attachments]) detachOne();
212
+ }
213
+ return {
214
+ ws,
215
+ ready,
216
+ address: () => activeServer?.address() ?? null,
217
+ handleUpgrade: sharedUpgradeListener,
218
+ attach: (target) => attachTo(target, sharedUpgradeListener),
219
+ detach: detachAll,
220
+ async close() {
221
+ detachAll();
222
+ ws.closeAll(void 0, void 0, true);
223
+ if (ownedServer) {
224
+ const srv = ownedServer;
225
+ await ready.catch(() => {});
226
+ if (!srv.listening) return;
227
+ await new Promise((r) => srv.close(() => r()));
228
+ }
229
+ }
230
+ };
231
+ }
232
+ //#endregion
2
233
  export { attachWsRpcTransport, createWsOriginRegistry, createWsRpcPeerHooks, isAllowedOrigin, isLoopbackHostname };
@@ -1,12 +1,12 @@
1
1
  import { n as __exportAll } from "./rolldown-runtime-B4iAMlE-.mjs";
2
2
  import { createRpcServer } from "./rpc/server.mjs";
3
- import { t as diagnostics } from "./diagnostics-BXWW3VK-.mjs";
3
+ import { t as diagnostics } from "./diagnostics-DZA6KQ_Z.mjs";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
5
  //#region src/node/rpc-core.ts
6
6
  var rpc_core_exports = /* @__PURE__ */ __exportAll({ createContextRpcServer: () => createContextRpcServer });
7
7
  /**
8
8
  * Bind a devframe context's registered RPC functions to a birpc group,
9
- * transport-agnostically — the shared core under the instance shell's own
9
+ * transport-agnostically: the shared core under the instance shell's own
10
10
  * HTTP+WS binding (Node http + WS) and the Bun fetch-upgrade tier of
11
11
  * `createHandler`.
12
12
  *
@@ -26,8 +26,20 @@ function createContextRpcServer(options) {
26
26
  for (const fn of authHandler.rpcFunctions) if (!rpcHost.definitions.has(fn.name)) rpcHost.register(fn);
27
27
  }
28
28
  const rpcGroup = createRpcServer(rpcHost.functions, { rpcOptions: {
29
+ /**
30
+ * Forwarded as-is so a host with its own structured diagnostics
31
+ * keeps seeing RPC failures.
32
+ */
29
33
  onFunctionError: options.rpcOptions?.onFunctionError,
30
34
  onGeneralError: options.rpcOptions?.onGeneralError,
35
+ /**
36
+ * Wrap each RPC handler in an AsyncLocalStorage context so
37
+ * `ctx.rpc.getCurrentRpcSession()` works inside handlers (used
38
+ * by streaming subscribe/unsubscribe/cancel and shared-state
39
+ * sync), and (when an `authorize` gate is configured) reject
40
+ * the call before it ever reaches the handler. Mirrors
41
+ * `packages/core/src/node/ws.ts`'s resolver.
42
+ */
31
43
  resolver(name, fn) {
32
44
  const rpc = this;
33
45
  if (!fn) return void 0;
@@ -1,6 +1,6 @@
1
- import { d as DevframeRpcConnection, u as DevframeNodeRpcSessionMeta } from "./ws-server-VyHlanzy.mjs";
2
- import { Ct as DevframeAuthHandler, _t as DevframeRpcServerFunctions, gt as DevframeRpcClientFunctions, o as DevframeNodeContext, s as DevframeNodeRpcSession } from "./context-CLF4-8Pp.mjs";
3
- import "./index-D3k0BIhe.mjs";
1
+ import { c as DevframeNodeRpcSessionMeta, l as DevframeRpcConnection } from "./ws-server-D2nv68Hg.mjs";
2
+ import { Tt as DevframeAuthHandler, o as DevframeNodeContext, s as DevframeNodeRpcSession, vt as DevframeRpcClientFunctions, yt as DevframeRpcServerFunctions } from "./context-CHW4BZOj.mjs";
3
+ import "./index-CNC390b_.mjs";
4
4
  import { BirpcGroup, EventOptions } from "birpc";
5
5
  //#region src/node/rpc-core.d.ts
6
6
  interface CreateContextRpcServerOptions {
@@ -33,7 +33,7 @@ interface ContextRpcServer {
33
33
  }
34
34
  /**
35
35
  * Bind a devframe context's registered RPC functions to a birpc group,
36
- * transport-agnostically — the shared core under the instance shell's own
36
+ * transport-agnostically: the shared core under the instance shell's own
37
37
  * HTTP+WS binding (Node http + WS) and the Bun fetch-upgrade tier of
38
38
  * `createHandler`.
39
39
  *
@@ -0,0 +1,116 @@
1
+ import { i as defineDiagnostics } from "./nostics-D0PvLZsn.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
+ //#region src/rpc/serialization.ts
40
+ /**
41
+ * Wire format used by the live RPC transports (WebSocket and SSE).
42
+ *
43
+ * - **JSON (default, unprefixed):** payload is plain JSON text. Used when
44
+ * the dispatched method is declared `jsonSerializable: true`. Encoded
45
+ * via {@link strictJsonStringify} (rejects non-JSON values), decoded
46
+ * via `JSON.parse`.
47
+ * - **Structured-clone (`s:` prefix):** payload is `s:` followed by
48
+ * `structured-clone-es` text. Used when the method is declared
49
+ * `jsonSerializable: false` (or omitted, the default). Round-trips
50
+ * `Map`, `Set`, `Date`, `BigInt`, cycles, and class instances.
51
+ *
52
+ * birpc envelopes always start with `{`, so a leading byte that is not
53
+ * `s` is unambiguously JSON. Each direction independently chooses its
54
+ * encoding from local definitions; request and response are not
55
+ * coupled by a mirror rule.
56
+ */
57
+ const STRUCTURED_CLONE_PREFIX = "s:";
58
+ /**
59
+ * `JSON.stringify` with a single-pass strict replacer.
60
+ *
61
+ * Throws `DF0020` synchronously when the value contains a type JSON
62
+ * cannot round-trip losslessly: `Map`, `Set`, `Date`, `BigInt`, class
63
+ * instances, or `undefined` inside an array (silently becomes `null`).
64
+ *
65
+ * Native pass-throughs (no extra work needed):
66
+ * - circular references: `JSON.stringify` raises `TypeError`.
67
+ * - `BigInt` at top level: caught here for a friendlier error path.
68
+ *
69
+ * Lenient cases (allowed without throwing):
70
+ * - `undefined` as an object property: legitimate optional field;
71
+ * JSON.stringify just omits it.
72
+ * - `undefined` at the root: legitimate "action returned nothing".
73
+ * - `Symbol` / `Function` values: semantically "drop me" in JSON.
74
+ *
75
+ * `fnName` is used only for the diagnostic message; pass the RPC
76
+ * function name when calling from a wire serializer / dump writer so
77
+ * the error points at the offending function.
78
+ */
79
+ function strictJsonStringify(value, fnName = "") {
80
+ return JSON.stringify(value, function strictReplacer(key, val) {
81
+ const holder = this;
82
+ const original = holder != null ? holder[key] : val;
83
+ if (original === void 0) {
84
+ if (Array.isArray(holder)) throw nonJsonAt(fnName, "undefined", holder, key);
85
+ return val;
86
+ }
87
+ if (original !== null) assertJsonSafe(original, holder, key, fnName);
88
+ return val;
89
+ });
90
+ }
91
+ /** Throw `DF0020` when `original` is a type JSON can't round-trip losslessly. */
92
+ function assertJsonSafe(original, holder, key, fnName) {
93
+ if (typeof original === "bigint") throw nonJsonAt(fnName, "BigInt", holder, key);
94
+ if (typeof original !== "object") return;
95
+ if (original instanceof Map) throw nonJsonAt(fnName, "Map", holder, key);
96
+ if (original instanceof Set) throw nonJsonAt(fnName, "Set", holder, key);
97
+ if (original instanceof Date) throw nonJsonAt(fnName, "Date", holder, key);
98
+ if (Array.isArray(original)) return;
99
+ const proto = Object.getPrototypeOf(original);
100
+ if (proto !== null && proto !== Object.prototype) throw nonJsonAt(fnName, original.constructor?.name ?? "class instance", holder, key);
101
+ }
102
+ function nonJsonAt(fnName, type, parent, key) {
103
+ const path = formatPath(parent, key);
104
+ return diagnostics.DF0020({
105
+ name: fnName || "<anonymous>",
106
+ type,
107
+ path
108
+ });
109
+ }
110
+ function formatPath(parent, key) {
111
+ if (Array.isArray(parent)) return `[${key}]`;
112
+ if (key === "") return "<root>";
113
+ return key;
114
+ }
115
+ //#endregion
116
+ export { strictJsonStringify as n, diagnostics as r, STRUCTURED_CLONE_PREFIX as t };
@@ -0,0 +1,154 @@
1
+ import { createRequire } from "node:module";
2
+ import { isAbsolute, join } from "pathe";
3
+ import { pathToFileURL } from "node:url";
4
+ //#region src/node/services-install.ts
5
+ /**
6
+ * Turn a `resolveFrom` value (a file path, a file URL like `import.meta.url`,
7
+ * or a directory) into something `createRequire` accepts; a directory gets a
8
+ * synthetic filename appended so resolution starts inside it.
9
+ */
10
+ function toRequireBase(resolveFrom) {
11
+ if (resolveFrom.startsWith("file://")) return resolveFrom;
12
+ if ((resolveFrom.split(/[/\\]/).pop() ?? "").includes(".")) return resolveFrom;
13
+ return join(resolveFrom, "_devframe_resolve.js");
14
+ }
15
+ /**
16
+ * Normalize an `install()` `resolveFrom` into a resolution base. Paths and
17
+ * file URLs pass through (the common case: the declaring plugin's
18
+ * `importMetaUrl`, so a service it declares resolves against the plugin's own
19
+ * dependencies); a bare npm package name resolves to that package's location
20
+ * from `cwd`. An unresolvable package name reads as no base (the caller's
21
+ * workspace fallbacks apply).
22
+ */
23
+ function expandResolveFrom(resolveFrom, cwd) {
24
+ if (resolveFrom.startsWith("file://") || resolveFrom.startsWith(".") || isAbsolute(resolveFrom)) return resolveFrom;
25
+ const require = createRequire(join(cwd, "_devframe_resolve.js"));
26
+ try {
27
+ return require.resolve(`${resolveFrom}/package.json`);
28
+ } catch {}
29
+ try {
30
+ return require.resolve(resolveFrom);
31
+ } catch {}
32
+ }
33
+ /**
34
+ * Import a service package's module, trying each `resolveFrom` candidate in
35
+ * order (so a plugin-declared service resolves against the plugin's own
36
+ * dependency tree first, then the workspace fallback). Throws the last
37
+ * resolution error when no candidate succeeds.
38
+ */
39
+ async function importServicePackage(pkg, resolveFroms) {
40
+ const candidates = [...new Set(resolveFroms.filter((x) => typeof x === "string" && x.length > 0))];
41
+ let lastError = /* @__PURE__ */ new Error(`no resolution base available for "${pkg}"`);
42
+ for (const from of candidates) {
43
+ let resolved;
44
+ try {
45
+ resolved = createRequire(toRequireBase(from)).resolve(pkg);
46
+ } catch (error) {
47
+ lastError = error;
48
+ continue;
49
+ }
50
+ return await import(
51
+ /* webpackIgnore: true */
52
+ /* @vite-ignore */
53
+ /* turbopackIgnore: true */
54
+ pathToFileURL(resolved).href
55
+ );
56
+ }
57
+ throw lastError;
58
+ }
59
+ function parseVersion(input) {
60
+ const [core, ...prerelease] = input.trim().replace(/^v/, "").split("-");
61
+ if (!core) return void 0;
62
+ const parts = core.split(".").map((part) => Number.parseInt(part, 10));
63
+ if (parts.length === 0 || parts.some((part) => Number.isNaN(part) || part < 0)) return void 0;
64
+ while (parts.length < 3) parts.push(0);
65
+ return {
66
+ parts,
67
+ ...prerelease.length ? { prerelease: prerelease.join("-") } : {}
68
+ };
69
+ }
70
+ function compareVersions(a, b) {
71
+ for (let i = 0; i < 3; i++) {
72
+ const diff = (a.parts[i] ?? 0) - (b.parts[i] ?? 0);
73
+ if (diff !== 0) return diff;
74
+ }
75
+ if (a.prerelease && !b.prerelease) return -1;
76
+ if (!a.prerelease && b.prerelease) return 1;
77
+ if (a.prerelease && b.prerelease) return a.prerelease < b.prerelease ? -1 : a.prerelease > b.prerelease ? 1 : 0;
78
+ return 0;
79
+ }
80
+ /** Whether `version` and `base` share their first `lockUpTo + 1` parts. */
81
+ function partsMatchUpTo(version, base, lockUpTo) {
82
+ for (let i = 0; i <= lockUpTo; i++) if ((version.parts[i] ?? 0) !== (base.parts[i] ?? 0)) return false;
83
+ return true;
84
+ }
85
+ function satisfiesCaret(version, base) {
86
+ if (compareVersions(version, base) < 0) return false;
87
+ const fixedIndex = base.parts.findIndex((part) => part !== 0);
88
+ return partsMatchUpTo(version, base, fixedIndex === -1 ? base.parts.length - 1 : fixedIndex);
89
+ }
90
+ function satisfiesTilde(version, base, segments) {
91
+ if (compareVersions(version, base) < 0) return false;
92
+ return partsMatchUpTo(version, base, segments.length >= 2 ? 1 : 0);
93
+ }
94
+ function satisfiesExactOrPrefix(version, base, segments) {
95
+ for (let i = 0; i < Math.max(segments.length, 3); i++) if (i < segments.length && (version.parts[i] ?? 0) !== (base.parts[i] ?? 0)) return false;
96
+ return segments.length >= 3 ? compareVersions(version, base) === 0 : true;
97
+ }
98
+ function satisfiesComparator(version, comparator) {
99
+ const raw = comparator.trim();
100
+ if (!raw || raw === "*" || raw === "x") return true;
101
+ const operatorMatch = raw.match(/^([\^~]|>=|<=|[><=])?(.+)$/);
102
+ if (!operatorMatch) return false;
103
+ const operator = operatorMatch[1];
104
+ const rest = operatorMatch[2].trim();
105
+ const segments = rest.replace(/\.[x*]/gi, "").split(".").filter(Boolean);
106
+ const base = parseVersion(rest.replace(/[x*]/gi, "0"));
107
+ if (!base) return false;
108
+ switch (operator) {
109
+ case ">": return compareVersions(version, base) > 0;
110
+ case ">=": return compareVersions(version, base) >= 0;
111
+ case "<": return compareVersions(version, base) < 0;
112
+ case "<=": return compareVersions(version, base) <= 0;
113
+ case "^": return satisfiesCaret(version, base);
114
+ case "~": return satisfiesTilde(version, base, segments);
115
+ default: return satisfiesExactOrPrefix(version, base, segments);
116
+ }
117
+ }
118
+ /**
119
+ * Pragmatic semver range check for service version declarations; supports
120
+ * the common forms (`1.2.3`, `^1.2.3`, `~1.2`, `>=1 <3`, `1.x`, `*`, and
121
+ * `||`-joined alternatives) without pulling in a semver dependency. An
122
+ * unparseable version or range reads as **not satisfied**.
123
+ */
124
+ function satisfiesVersionRange(version, range) {
125
+ const parsed = parseVersion(version);
126
+ if (!parsed) return false;
127
+ const alternatives = range.split("||").map((alt) => alt.trim()).filter(Boolean);
128
+ if (alternatives.length === 0) return true;
129
+ return alternatives.some((alternative) => alternative.split(/\s+/).every((comparator) => satisfiesComparator(parsed, comparator)));
130
+ }
131
+ function isPlainObject(value) {
132
+ return typeof value === "object" && value !== null && !Array.isArray(value);
133
+ }
134
+ /** Deep-merge two values with the service option-set rules (see below). */
135
+ function deepMergeTwo(a, b) {
136
+ if (Array.isArray(a) && Array.isArray(b)) return [.../* @__PURE__ */ new Set([...a, ...b])];
137
+ if (isPlainObject(a) && isPlainObject(b)) {
138
+ const out = { ...a };
139
+ for (const key of Object.keys(b)) out[key] = key in a ? deepMergeTwo(a[key], b[key]) : b[key];
140
+ return out;
141
+ }
142
+ return b;
143
+ }
144
+ /**
145
+ * Default option-set merge when a service declares no `mergeOptions`:
146
+ * deep-merge in declaration order: objects recurse, arrays union-dedupe,
147
+ * scalars take the later value. Covers the built-in services (`roots` /
148
+ * `langs` union, `themes` per-key last-wins) without a custom hook.
149
+ */
150
+ function deepMergeOptionSets(sets) {
151
+ return sets.reduce((merged, set) => deepMergeTwo(merged, set));
152
+ }
153
+ //#endregion
154
+ export { satisfiesVersionRange as i, expandResolveFrom as n, importServicePackage as r, deepMergeOptionSets as t };
@@ -0,0 +1,16 @@
1
+ //#region src/rpc/transports/session.ts
2
+ let sessionId = 0;
3
+ /**
4
+ * Mint the per-connection session meta every transport binding shares:
5
+ * one id space across transports, so session bookkeeping (streaming
6
+ * subscriptions, shared-state sync, auth trust) never collides between a
7
+ * WS peer and an SSE session on the same server.
8
+ */
9
+ function createRpcSessionMeta() {
10
+ return {
11
+ id: sessionId++,
12
+ subscribedStates: /* @__PURE__ */ new Set()
13
+ };
14
+ }
15
+ //#endregion
16
+ export { createRpcSessionMeta as t };
@@ -931,8 +931,8 @@ function createSharedState(options) {
931
931
  if (syncIds.has(syncId)) return;
932
932
  rememberSyncId(syncIds, syncId);
933
933
  if (enablePatches$1) {
934
- const [newState, patches] = produceWithPatches(state, fn);
935
- state = newState;
934
+ const [nextState, patches] = produceWithPatches(state, fn);
935
+ state = nextState;
936
936
  events.emit("updated", state, patches, syncId);
937
937
  } else {
938
938
  state = produce(state, fn);
@@ -37,7 +37,7 @@ function number() {
37
37
  function boolean() {
38
38
  return make("boolean", (v) => typeof v === "boolean" ? ok(v) : fail("Expected a boolean"));
39
39
  }
40
- /** `undefined` — mirrors valibot's `void`. */
40
+ /** `undefined`, mirroring valibot's `void`. */
41
41
  function voidType() {
42
42
  return make("void", (v) => v === void 0 ? ok(void 0) : fail("Expected undefined"));
43
43
  }
@@ -130,7 +130,7 @@ function describe(schema, description) {
130
130
  };
131
131
  }
132
132
  /**
133
- * Grouped access to every builder — `s.string()`, `s.object({ ... })`,
133
+ * Grouped access to every builder: `s.string()`, `s.object({ ... })`,
134
134
  * `s.void()`, etc. Handy for a valibot-like `import { s } from
135
135
  * 'devframe/utils/simple-schema'` call site.
136
136
  */
@@ -1,4 +1,4 @@
1
- import { v as RpcFunctionDefinitionAny } from "./types-CMdPYhOx.mjs";
1
+ import { v as RpcFunctionDefinitionAny } from "./types-Dq4i156u.mjs";
2
2
  import { ChannelOptions } from "birpc";
3
3
  //#region src/rpc/transports/sse-client.d.ts
4
4
  interface SseRpcChannelOptions {
@@ -10,7 +10,7 @@ interface SseRpcChannelOptions {
10
10
  authToken?: string;
11
11
  /**
12
12
  * RPC function definitions (or just the `jsonSerializable` flag per
13
- * method) used to dispatch the per-call wire serializer — same contract
13
+ * method) used to dispatch the per-call wire serializer, the same contract
14
14
  * as the WS channel.
15
15
  */
16
16
  definitions?: ReadonlyMap<string, Pick<RpcFunctionDefinitionAny, 'jsonSerializable'>>;
@@ -19,7 +19,7 @@ interface SseRpcChannelOptions {
19
19
  }
20
20
  /**
21
21
  * Build a birpc `ChannelOptions` object backed by an SSE stream (server →
22
- * client) and HTTP `POST` (client → server) — the WebSocket-free counterpart
22
+ * client) and HTTP `POST` (client → server): the WebSocket-free counterpart
23
23
  * to `createWsRpcChannel`, wire-compatible with `attachSseRpcTransport`.
24
24
  *
25
25
  * The stream is consumed via `fetch` streaming (not `EventSource`), so it
@@ -27,7 +27,7 @@ interface SseRpcChannelOptions {
27
27
  * (`event: session`) carries the session id; every `POST` echoes it in the
28
28
  * `x-birpc-session` header. A response to a client-initiated request comes
29
29
  * back in the `POST`'s own body and is re-injected into the channel; a
30
- * dropped stream terminates the channel — there is no reconnect, matching
30
+ * dropped stream terminates the channel; there is no reconnect, matching
31
31
  * the WS channel's closed-is-done semantics.
32
32
  */
33
33
  declare function createSseRpcChannel(options: SseRpcChannelOptions): ChannelOptions & {