devframe 0.8.2 → 0.9.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_shared-CttmCMBi.mjs +70 -0
- package/dist/_shared-DmpWW-HH.d.mts +43 -0
- package/dist/adapters/build.d.mts +1 -1
- package/dist/adapters/build.mjs +6 -5
- package/dist/adapters/cac.d.mts +1 -1
- package/dist/adapters/cac.mjs +157 -1
- package/dist/adapters/dev.d.mts +20 -42
- package/dist/adapters/dev.mjs +2 -1
- package/dist/adapters/embedded.d.mts +1 -1
- package/dist/adapters/initiate.d.mts +223 -0
- package/dist/adapters/initiate.mjs +2 -0
- package/dist/adapters/mcp.d.mts +26 -2
- package/dist/adapters/mcp.mjs +2 -2
- package/dist/cli/main.mjs +2 -2
- package/dist/client/index.d.mts +70 -6
- package/dist/client/index.mjs +743 -408
- package/dist/constants.d.mts +19 -6
- package/dist/constants.mjs +19 -6
- package/dist/{context-CBg8iRbQ.mjs → context-C33rR7n0.mjs} +1 -1
- package/dist/{context-DoSozfvx.d.mts → context-CSCwoABx.d.mts} +1 -1
- package/dist/{host-h3-D2hYZvDK.mjs → context-Du11TLcO.mjs} +5 -255
- package/dist/dev-BEveeA5H.mjs +233 -0
- package/dist/{devframe-mQ4qZAuo.d.mts → devframe-Bdd9YXTy.d.mts} +88 -25
- package/dist/{diagnostics-D7wM2cwM.mjs → diagnostics-DLYH5oD3.mjs} +16 -0
- package/dist/dist-D8jAMwxU.mjs +227 -0
- package/dist/helpers/vite.d.mts +43 -39
- package/dist/helpers/vite.mjs +26 -38
- package/dist/host-agent-CpbAoqBx.mjs +230 -0
- package/dist/host-h3-CzCZwtuV.mjs +26 -0
- package/dist/{fetch-BKaD0Vb3.mjs → http-DLr_cJZ7.mjs} +42 -4
- package/dist/{index-b64Uuhy2.d.mts → index-CZRd2K1C.d.mts} +2 -2
- package/dist/{index-Dbw1p5ch.d.mts → index-DSCq4M4q.d.mts} +2 -31
- package/dist/index.d.mts +9 -4
- package/dist/index.mjs +8 -1
- package/dist/{instance-registry-BnnA2Eoh.mjs → instance-registry-BQqGoDO9.mjs} +9 -2
- package/dist/instance-shell-DBTXTSjN.d.mts +264 -0
- package/dist/instance-shell-KRdee1QF.mjs +485 -0
- package/dist/internal/index.d.mts +150 -0
- package/dist/internal/index.mjs +7 -0
- package/dist/node/auth.d.mts +2 -2
- package/dist/node/hub-internals.d.mts +2 -14
- package/dist/node/hub-internals.mjs +2 -2
- package/dist/node/index.d.mts +3 -267
- package/dist/node/index.mjs +3 -5
- package/dist/recipes/common-rpc-functions.d.mts +3 -3
- package/dist/recipes/interactive-auth.d.mts +3 -3
- package/dist/recipes/interactive-auth.mjs +6 -6
- package/dist/rolldown-runtime-JspESFgx.mjs +35 -0
- package/dist/rpc/dump.d.mts +77 -1
- package/dist/rpc/dump.mjs +1 -1
- package/dist/rpc/index.d.mts +2 -2
- package/dist/rpc/index.mjs +3 -17
- package/dist/rpc/transports/sse-client.d.mts +2 -0
- package/dist/rpc/transports/sse-client.mjs +142 -0
- package/dist/rpc/transports/sse-server.d.mts +61 -0
- package/dist/rpc/transports/sse-server.mjs +209 -0
- package/dist/rpc/transports/ws-bun.d.mts +35 -0
- package/dist/rpc/transports/ws-bun.mjs +31 -0
- package/dist/rpc/transports/ws-client.d.mts +1 -1
- package/dist/rpc/transports/ws-client.mjs +10 -18
- package/dist/rpc/transports/ws-server.d.mts +2 -2
- package/dist/rpc/transports/ws-server.mjs +2 -235
- package/dist/rpc-core-CGctXToO.d.mts +48 -0
- package/dist/rpc-core-CqcJkAu9.mjs +81 -0
- package/dist/{serialization-C8Mnw9hK.mjs → serialization-BHY8M5NQ.mjs} +1 -1
- package/dist/{shared-state-6kHolYZy.mjs → shared-state-fRRbRUtD.mjs} +1 -1
- package/dist/sse-client-BsIK3hMD.d.mts +37 -0
- package/dist/{dump-CgZShDRB.mjs → static-CvjkO8QE.mjs} +2 -265
- package/dist/{storage-CTNVbuFB.mjs → storage-BcmVEJRc.mjs} +2 -2
- package/dist/types/index.d.mts +3 -3
- package/dist/types/index.mjs +1 -6
- package/dist/utils/events.d.mts +1 -1
- package/dist/utils/launch-editor.mjs +1 -25
- package/dist/utils/open.mjs +1 -1
- package/dist/utils/serve-static.mjs +1 -1
- package/dist/utils/shared-state.d.mts +1 -1
- package/dist/utils/shared-state.mjs +1 -1
- package/dist/utils/simple-schema.d.mts +101 -1
- package/dist/utils/streaming-channel.d.mts +1 -1
- package/dist/validation-BiuDXTKW.mjs +266 -0
- package/dist/wire-codec-9VhIKnyp.mjs +58 -0
- package/dist/{ws-client-B6Tnr0ic.d.mts → ws-client-CdNuATYa.d.mts} +6 -1
- package/dist/ws-server-Bk1hOG3X.mjs +267 -0
- package/dist/{ws-server-uKQcClcJ.d.mts → ws-server-Ces08fjP.d.mts} +89 -8
- package/package.json +10 -9
- package/skills/devframe/SKILL.md +84 -85
- package/dist/_shared-bWRzeSa0.mjs +0 -19
- package/dist/adapters/cli.d.mts +0 -11
- package/dist/adapters/cli.mjs +0 -6
- package/dist/cac-D4_9ok0z.mjs +0 -157
- package/dist/dev-Bc49u3qg.mjs +0 -439
- package/dist/http-C4T68YV6.mjs +0 -38
- package/dist/index-vAX6bYuc.d.mts +0 -78
- package/dist/recipes/open-helpers.d.mts +0 -6
- package/dist/recipes/open-helpers.mjs +0 -6
- package/dist/server-CD8XmBXl.mjs +0 -167
- package/dist/server-UHWae43Q.d.mts +0 -149
- package/dist/simple-schema-BDzLeJDk.d.mts +0 -102
- package/dist/utils/promise.d.mts +0 -8
- package/dist/utils/promise.mjs +0 -15
- package/dist/utils/scope.d.mts +0 -11
- package/dist/utils/scope.mjs +0 -15
- package/dist/{open-Deb5xmIT.mjs → open-Dz4V4irZ.mjs} +1 -1
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { t as diagnostics } from "./diagnostics-hwjXp_UV.mjs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
//#region ../../node_modules/.pnpm/ohash@2.0.11/node_modules/ohash/dist/shared/ohash.D__AXeF1.mjs
|
|
4
|
+
function serialize(o) {
|
|
5
|
+
return typeof o == "string" ? `'${o}'` : new c().serialize(o);
|
|
6
|
+
}
|
|
7
|
+
const c = /*@__PURE__*/ function() {
|
|
8
|
+
class o {
|
|
9
|
+
#t = /* @__PURE__ */ new Map();
|
|
10
|
+
compare(t, r) {
|
|
11
|
+
const e = typeof t, n = typeof r;
|
|
12
|
+
return e === "string" && n === "string" ? t.localeCompare(r) : e === "number" && n === "number" ? t - r : String.prototype.localeCompare.call(this.serialize(t, true), this.serialize(r, true));
|
|
13
|
+
}
|
|
14
|
+
serialize(t, r) {
|
|
15
|
+
if (t === null) return "null";
|
|
16
|
+
switch (typeof t) {
|
|
17
|
+
case "string": return r ? t : `'${t}'`;
|
|
18
|
+
case "bigint": return `${t}n`;
|
|
19
|
+
case "object": return this.$object(t);
|
|
20
|
+
case "function": return this.$function(t);
|
|
21
|
+
}
|
|
22
|
+
return String(t);
|
|
23
|
+
}
|
|
24
|
+
serializeObject(t) {
|
|
25
|
+
const r = Object.prototype.toString.call(t);
|
|
26
|
+
if (r !== "[object Object]") return this.serializeBuiltInType(r.length < 10 ? `unknown:${r}` : r.slice(8, -1), t);
|
|
27
|
+
const e = t.constructor, n = e === Object || e === void 0 ? "" : e.name;
|
|
28
|
+
if (n !== "" && globalThis[n] === e) return this.serializeBuiltInType(n, t);
|
|
29
|
+
if (typeof t.toJSON == "function") {
|
|
30
|
+
const i = t.toJSON();
|
|
31
|
+
return n + (i !== null && typeof i == "object" ? this.$object(i) : `(${this.serialize(i)})`);
|
|
32
|
+
}
|
|
33
|
+
return this.serializeObjectEntries(n, Object.entries(t));
|
|
34
|
+
}
|
|
35
|
+
serializeBuiltInType(t, r) {
|
|
36
|
+
const e = this["$" + t];
|
|
37
|
+
if (e) return e.call(this, r);
|
|
38
|
+
if (typeof r?.entries == "function") return this.serializeObjectEntries(t, r.entries());
|
|
39
|
+
throw new Error(`Cannot serialize ${t}`);
|
|
40
|
+
}
|
|
41
|
+
serializeObjectEntries(t, r) {
|
|
42
|
+
const e = Array.from(r).sort((i, a) => this.compare(i[0], a[0]));
|
|
43
|
+
let n = `${t}{`;
|
|
44
|
+
for (let i = 0; i < e.length; i++) {
|
|
45
|
+
const [a, l] = e[i];
|
|
46
|
+
n += `${this.serialize(a, true)}:${this.serialize(l)}`, i < e.length - 1 && (n += ",");
|
|
47
|
+
}
|
|
48
|
+
return n + "}";
|
|
49
|
+
}
|
|
50
|
+
$object(t) {
|
|
51
|
+
let r = this.#t.get(t);
|
|
52
|
+
return r === void 0 && (this.#t.set(t, `#${this.#t.size}`), r = this.serializeObject(t), this.#t.set(t, r)), r;
|
|
53
|
+
}
|
|
54
|
+
$function(t) {
|
|
55
|
+
const r = Function.prototype.toString.call(t);
|
|
56
|
+
return r.slice(-15) === "[native code] }" ? `${t.name || ""}()[native]` : `${t.name}(${t.length})${r.replace(/\s*\n\s*/g, "")}`;
|
|
57
|
+
}
|
|
58
|
+
$Array(t) {
|
|
59
|
+
let r = "[";
|
|
60
|
+
for (let e = 0; e < t.length; e++) r += this.serialize(t[e]), e < t.length - 1 && (r += ",");
|
|
61
|
+
return r + "]";
|
|
62
|
+
}
|
|
63
|
+
$Date(t) {
|
|
64
|
+
try {
|
|
65
|
+
return `Date(${t.toISOString()})`;
|
|
66
|
+
} catch {
|
|
67
|
+
return "Date(null)";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
$ArrayBuffer(t) {
|
|
71
|
+
return `ArrayBuffer[${new Uint8Array(t).join(",")}]`;
|
|
72
|
+
}
|
|
73
|
+
$Set(t) {
|
|
74
|
+
return `Set${this.$Array(Array.from(t).sort((r, e) => this.compare(r, e)))}`;
|
|
75
|
+
}
|
|
76
|
+
$Map(t) {
|
|
77
|
+
return this.serializeObjectEntries("Map", t.entries());
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const s of [
|
|
81
|
+
"Error",
|
|
82
|
+
"RegExp",
|
|
83
|
+
"URL"
|
|
84
|
+
]) o.prototype["$" + s] = function(t) {
|
|
85
|
+
return `${s}(${t})`;
|
|
86
|
+
};
|
|
87
|
+
for (const s of [
|
|
88
|
+
"Int8Array",
|
|
89
|
+
"Uint8Array",
|
|
90
|
+
"Uint8ClampedArray",
|
|
91
|
+
"Int16Array",
|
|
92
|
+
"Uint16Array",
|
|
93
|
+
"Int32Array",
|
|
94
|
+
"Uint32Array",
|
|
95
|
+
"Float32Array",
|
|
96
|
+
"Float64Array"
|
|
97
|
+
]) o.prototype["$" + s] = function(t) {
|
|
98
|
+
return `${s}[${t.join(",")}]`;
|
|
99
|
+
};
|
|
100
|
+
for (const s of ["BigInt64Array", "BigUint64Array"]) o.prototype["$" + s] = function(t) {
|
|
101
|
+
return `${s}[${t.join("n,")}${t.length > 0 ? "n" : ""}]`;
|
|
102
|
+
};
|
|
103
|
+
return o;
|
|
104
|
+
}();
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region ../../node_modules/.pnpm/ohash@2.0.11/node_modules/ohash/dist/crypto/node/index.mjs
|
|
107
|
+
const e = globalThis.process?.getBuiltinModule?.("crypto")?.hash;
|
|
108
|
+
const r = "sha256";
|
|
109
|
+
const s = "base64url";
|
|
110
|
+
function digest(t) {
|
|
111
|
+
if (e) return e(r, t, s);
|
|
112
|
+
const o = createHash(r).update(t);
|
|
113
|
+
return globalThis.process?.versions?.webcontainer ? o.digest().toString(s) : o.digest(s);
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region ../../node_modules/.pnpm/ohash@2.0.11/node_modules/ohash/dist/index.mjs
|
|
117
|
+
function hash$1(input) {
|
|
118
|
+
return digest(serialize(input));
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/utils/hash.ts
|
|
122
|
+
/**
|
|
123
|
+
* Stable, deterministic hash of any structured-cloneable value.
|
|
124
|
+
*/
|
|
125
|
+
function hash(value) {
|
|
126
|
+
return hash$1(value);
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/rpc/validate-io.ts
|
|
130
|
+
/**
|
|
131
|
+
* Run a single [Standard Schema](https://standardschema.dev) validator,
|
|
132
|
+
* awaiting the result when the validator is asynchronous.
|
|
133
|
+
*/
|
|
134
|
+
async function runStandardSchema(schema, value) {
|
|
135
|
+
const result = schema["~standard"].validate(value);
|
|
136
|
+
return result instanceof Promise ? await result : result;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Render Standard Schema issues into a single human-readable line for a
|
|
140
|
+
* diagnostic message, prefixing each with its dotted path when present.
|
|
141
|
+
*/
|
|
142
|
+
function formatIssues(issues) {
|
|
143
|
+
return issues.map((issue) => {
|
|
144
|
+
const path = issue.path?.map((segment) => typeof segment === "object" ? segment.key : segment).join(".");
|
|
145
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
146
|
+
}).join("; ");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Validate positional arguments against their declared schemas. Only
|
|
150
|
+
* indices with a schema are checked; extra arguments pass through
|
|
151
|
+
* untouched. Throws `DF0038` on the first failing argument.
|
|
152
|
+
*
|
|
153
|
+
* Validation guards the payload without rewriting it: the original values
|
|
154
|
+
* are handed to the handler unchanged, so a schema that describes a subset
|
|
155
|
+
* of an object never silently strips the sender's extra fields (and any
|
|
156
|
+
* declared transforms stay a purely type-level concern).
|
|
157
|
+
*
|
|
158
|
+
* @internal
|
|
159
|
+
*/
|
|
160
|
+
async function validateRpcArgs(name, argsSchema, args) {
|
|
161
|
+
const original = args.slice();
|
|
162
|
+
if (!argsSchema || argsSchema.length === 0) return original;
|
|
163
|
+
for (let index = 0; index < argsSchema.length; index++) {
|
|
164
|
+
const schema = argsSchema[index];
|
|
165
|
+
if (!schema) continue;
|
|
166
|
+
const result = await runStandardSchema(schema, args[index]);
|
|
167
|
+
if (result.issues) throw diagnostics.DF0043({
|
|
168
|
+
name,
|
|
169
|
+
index,
|
|
170
|
+
issues: formatIssues(result.issues)
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return original;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Validate a handler's resolved return value against its declared schema.
|
|
177
|
+
* Throws `DF0039` when the value fails the schema, otherwise returns the
|
|
178
|
+
* original value unchanged (guard-only, never rewriting the payload — see
|
|
179
|
+
* {@link validateRpcArgs}). Passes through when no return schema is set.
|
|
180
|
+
*
|
|
181
|
+
* @internal
|
|
182
|
+
*/
|
|
183
|
+
async function validateRpcReturn(name, returnSchema, value) {
|
|
184
|
+
if (!returnSchema) return value;
|
|
185
|
+
const result = await runStandardSchema(returnSchema, value);
|
|
186
|
+
if (result.issues) throw diagnostics.DF0044({
|
|
187
|
+
name,
|
|
188
|
+
issues: formatIssues(result.issues)
|
|
189
|
+
});
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region src/rpc/handler.ts
|
|
194
|
+
async function getRpcResolvedSetupResult(definition, context) {
|
|
195
|
+
if (!definition.setup) return {};
|
|
196
|
+
if (typeof context === "object" && context !== null) {
|
|
197
|
+
definition.__cache ??= /* @__PURE__ */ new WeakMap();
|
|
198
|
+
const cache = definition.__cache;
|
|
199
|
+
let promise = cache.get(context);
|
|
200
|
+
if (!promise) {
|
|
201
|
+
promise = Promise.resolve(definition.setup(context));
|
|
202
|
+
promise.catch(() => {
|
|
203
|
+
if (cache.get(context) === promise) cache.delete(context);
|
|
204
|
+
});
|
|
205
|
+
cache.set(context, promise);
|
|
206
|
+
}
|
|
207
|
+
return await promise;
|
|
208
|
+
}
|
|
209
|
+
if (!definition.__promise) {
|
|
210
|
+
const promise = Promise.resolve(definition.setup(context));
|
|
211
|
+
promise.catch(() => {
|
|
212
|
+
if (definition.__promise === promise) definition.__promise = void 0;
|
|
213
|
+
});
|
|
214
|
+
definition.__promise = promise;
|
|
215
|
+
}
|
|
216
|
+
return await definition.__promise;
|
|
217
|
+
}
|
|
218
|
+
async function getRpcHandler(definition, context) {
|
|
219
|
+
let handler = definition.handler;
|
|
220
|
+
if (!handler) {
|
|
221
|
+
const result = await getRpcResolvedSetupResult(definition, context);
|
|
222
|
+
if (!result.handler) throw diagnostics.DF0024({ name: definition.name });
|
|
223
|
+
handler = result.handler;
|
|
224
|
+
}
|
|
225
|
+
const argsSchema = definition.args;
|
|
226
|
+
const returnSchema = definition.returns;
|
|
227
|
+
if (!argsSchema && !returnSchema) return handler;
|
|
228
|
+
const inner = handler;
|
|
229
|
+
const validating = async (...args) => {
|
|
230
|
+
const validatedArgs = await validateRpcArgs(definition.name, argsSchema, args);
|
|
231
|
+
const output = await inner(...validatedArgs);
|
|
232
|
+
return await validateRpcReturn(definition.name, returnSchema, output);
|
|
233
|
+
};
|
|
234
|
+
return validating;
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/rpc/validation.ts
|
|
238
|
+
/**
|
|
239
|
+
* Validates RPC function definitions.
|
|
240
|
+
* Action and event functions cannot have dumps (side effects should not be cached).
|
|
241
|
+
*
|
|
242
|
+
* @throws {Error} If an action or event function has a dump configuration
|
|
243
|
+
*/
|
|
244
|
+
function validateDefinitions(definitions) {
|
|
245
|
+
for (const definition of definitions) {
|
|
246
|
+
const type = definition.type || "query";
|
|
247
|
+
if ((type === "action" || type === "event") && definition.dump) throw diagnostics.DF0027({
|
|
248
|
+
name: definition.name,
|
|
249
|
+
type
|
|
250
|
+
});
|
|
251
|
+
if (definition.snapshot && type !== "query") throw diagnostics.DF0028({
|
|
252
|
+
name: definition.name,
|
|
253
|
+
type
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Validates a single RPC function definition.
|
|
259
|
+
*
|
|
260
|
+
* @throws {Error} If an action or event function has a dump configuration
|
|
261
|
+
*/
|
|
262
|
+
function validateDefinition(definition) {
|
|
263
|
+
validateDefinitions([definition]);
|
|
264
|
+
}
|
|
265
|
+
//#endregion
|
|
266
|
+
export { validateRpcArgs as a, getRpcResolvedSetupResult as i, validateDefinitions as n, validateRpcReturn as o, getRpcHandler as r, hash as s, validateDefinition as t };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { n as strictJsonStringify } from "./serialization-BHY8M5NQ.mjs";
|
|
2
|
+
import { n as structuredCloneStringify, t as structuredCloneParse } from "./structured-clone-CbAV5rFI.mjs";
|
|
3
|
+
//#region src/rpc/wire-codec.ts
|
|
4
|
+
const EMPTY_WIRE_DEFS = /* @__PURE__ */ new Map();
|
|
5
|
+
/**
|
|
6
|
+
* Build the per-connection wire codec every live transport (WS server, WS
|
|
7
|
+
* client, SSE server, SSE client) shares: per-method dispatch between strict
|
|
8
|
+
* JSON (methods declared `jsonSerializable: true`) and `s:`-prefixed
|
|
9
|
+
* structured-clone (everything else, including all error envelopes), with a
|
|
10
|
+
* request-id → method map so a response independently picks the same
|
|
11
|
+
* encoder as its request. One codec per connection — request-id spaces
|
|
12
|
+
* don't collide across connections.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
* implementations; not part of the stable public API.
|
|
16
|
+
*/
|
|
17
|
+
function createRpcWireCodec(definitions = EMPTY_WIRE_DEFS) {
|
|
18
|
+
const pendingRequestMethods = /* @__PURE__ */ new Map();
|
|
19
|
+
return {
|
|
20
|
+
serialize: (msg) => {
|
|
21
|
+
let method;
|
|
22
|
+
if (msg.t === "q") method = msg.m;
|
|
23
|
+
else {
|
|
24
|
+
method = pendingRequestMethods.get(msg.i);
|
|
25
|
+
pendingRequestMethods.delete(msg.i);
|
|
26
|
+
}
|
|
27
|
+
if (!(msg.t === "s" && "e" in msg) && !!method && definitions.get(method)?.jsonSerializable === true) return strictJsonStringify(msg, method ?? "");
|
|
28
|
+
return `s:${structuredCloneStringify(msg)}`;
|
|
29
|
+
},
|
|
30
|
+
deserialize: (raw) => {
|
|
31
|
+
const msg = raw.startsWith("s:") ? structuredCloneParse(raw.slice(2)) : JSON.parse(raw);
|
|
32
|
+
if (msg.t === "q" && msg.i && msg.m) pendingRequestMethods.set(msg.i, msg.m);
|
|
33
|
+
return msg;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Peek at a wire frame's birpc envelope without engaging a codec's
|
|
39
|
+
* request-id bookkeeping — used by the SSE transport to route a frame
|
|
40
|
+
* (park a POST for its response / answer with a bare 202) before it is
|
|
41
|
+
* handed to birpc proper.
|
|
42
|
+
*
|
|
43
|
+
* @internal
|
|
44
|
+
* implementations; not part of the stable public API.
|
|
45
|
+
*/
|
|
46
|
+
function peekRpcWireFrame(raw) {
|
|
47
|
+
try {
|
|
48
|
+
const msg = raw.startsWith("s:") ? structuredCloneParse(raw.slice(2)) : JSON.parse(raw);
|
|
49
|
+
return {
|
|
50
|
+
t: msg?.t,
|
|
51
|
+
i: msg?.i
|
|
52
|
+
};
|
|
53
|
+
} catch {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
export { peekRpcWireFrame as n, createRpcWireCodec as t };
|
|
@@ -19,7 +19,12 @@ interface WsRpcChannelOptions {
|
|
|
19
19
|
/**
|
|
20
20
|
* Build a birpc `ChannelOptions` object backed by a browser `WebSocket`.
|
|
21
21
|
* Pass the result straight to `createRpcClient`'s `channel` option.
|
|
22
|
+
*
|
|
23
|
+
* Also returns `close()`, closing the underlying socket — mirroring the server transport's
|
|
24
|
+
* existing `WsRpcTransport.close()`. `birpc`'s own `ChannelOptions` has no teardown of its own.
|
|
22
25
|
*/
|
|
23
|
-
declare function createWsRpcChannel(options: WsRpcChannelOptions): ChannelOptions
|
|
26
|
+
declare function createWsRpcChannel(options: WsRpcChannelOptions): ChannelOptions & {
|
|
27
|
+
close: () => void;
|
|
28
|
+
};
|
|
24
29
|
//#endregion
|
|
25
30
|
export { createWsRpcChannel as n, WsRpcChannelOptions as t };
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import "./constants.mjs";
|
|
2
|
+
import { t as createRpcWireCodec } from "./wire-codec-9VhIKnyp.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
|
+
function isLoopbackHostname(hostname) {
|
|
87
|
+
const h = hostname.replace(/^\[|\]$/g, "");
|
|
88
|
+
return h === "localhost" || h === "127.0.0.1" || h === "::1" || h.endsWith(".localhost") || h.startsWith("127.");
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Default origin policy for a localhost dev tool: allow requests with no
|
|
92
|
+
* `Origin` header (native, non-browser clients), allow any loopback origin
|
|
93
|
+
* (so cross-port localhost dev setups keep working), and allow explicitly
|
|
94
|
+
* configured origins. Everything else — a real remote page in the dev's
|
|
95
|
+
* browser — is rejected.
|
|
96
|
+
*/
|
|
97
|
+
function isAllowedOrigin(origin, allowedOrigins) {
|
|
98
|
+
if (!origin) return true;
|
|
99
|
+
if (allowedOrigins.includes(origin)) return true;
|
|
100
|
+
try {
|
|
101
|
+
return isLoopbackHostname(new URL(origin).hostname);
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function isWsOriginRegistry(value) {
|
|
107
|
+
return !!value && !Array.isArray(value);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Build the `upgrade` listener that hands a request to the crossws adapter,
|
|
111
|
+
* optionally filtered to a single `path`. Non-matching requests are left
|
|
112
|
+
* untouched so other upgrade listeners (e.g. a Vite dev server's HMR socket)
|
|
113
|
+
* can claim them, unless `destroyUnmatched` is set.
|
|
114
|
+
*/
|
|
115
|
+
function createUpgradeListener(ws, path, destroyUnmatched, allowedOrigins) {
|
|
116
|
+
return (req, socket, head) => {
|
|
117
|
+
socket.on("error", () => {});
|
|
118
|
+
if (path) {
|
|
119
|
+
let pathname = req.url ?? "/";
|
|
120
|
+
try {
|
|
121
|
+
pathname = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
122
|
+
} catch {}
|
|
123
|
+
if (!pathMatches(pathname, path)) {
|
|
124
|
+
if (destroyUnmatched) {
|
|
125
|
+
socket.write("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
|
|
126
|
+
socket.destroy();
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const originAllowed = isWsOriginRegistry(allowedOrigins) ? allowedOrigins.isAllowed(req.headers.origin) : isAllowedOrigin(req.headers.origin, allowedOrigins || []);
|
|
132
|
+
if (allowedOrigins !== false && !originAllowed) {
|
|
133
|
+
socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
134
|
+
socket.destroy();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
ws.handleUpgrade(req, socket, head);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The per-peer lifecycle hooks driving a devframe RPC WebSocket, shaped for
|
|
142
|
+
* any [crossws](https://crossws.h3.dev) adapter. {@link attachWsRpcTransport}
|
|
143
|
+
* feeds them to the Node adapter; runtime-specific attachments (e.g. Bun's
|
|
144
|
+
* fetch-upgrade adapter) reuse the same hooks so every transport speaks the
|
|
145
|
+
* identical wire protocol — one birpc channel per peer, per-method
|
|
146
|
+
* `jsonSerializable` dispatch between strict JSON and structured-clone.
|
|
147
|
+
*/
|
|
148
|
+
function createWsRpcPeerHooks(rpcGroup, options = {}) {
|
|
149
|
+
const { onConnected = NOOP, onDisconnected = NOOP, definitions = EMPTY_DEFS, serialize: serializeOverride, deserialize: deserializeOverride } = options;
|
|
150
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
151
|
+
return {
|
|
152
|
+
open: (peer) => {
|
|
153
|
+
const meta = createRpcSessionMeta();
|
|
154
|
+
meta.peer = peer;
|
|
155
|
+
const connection = {
|
|
156
|
+
id: meta.id,
|
|
157
|
+
transport: "websocket",
|
|
158
|
+
request: peer.request,
|
|
159
|
+
send: (data) => peer.send(data),
|
|
160
|
+
close: (code, reason) => peer.close(code, reason),
|
|
161
|
+
peer
|
|
162
|
+
};
|
|
163
|
+
const codec = createRpcWireCodec(definitions);
|
|
164
|
+
const state = {
|
|
165
|
+
meta,
|
|
166
|
+
connection,
|
|
167
|
+
channel: void 0
|
|
168
|
+
};
|
|
169
|
+
const channel = {
|
|
170
|
+
post: (data) => {
|
|
171
|
+
peer.send(data);
|
|
172
|
+
},
|
|
173
|
+
on: (fn) => {
|
|
174
|
+
state.onMessage = fn;
|
|
175
|
+
},
|
|
176
|
+
serialize: serializeOverride ?? codec.serialize,
|
|
177
|
+
deserialize: deserializeOverride ?? codec.deserialize,
|
|
178
|
+
meta
|
|
179
|
+
};
|
|
180
|
+
state.channel = channel;
|
|
181
|
+
states.set(peer, state);
|
|
182
|
+
rpcGroup.updateChannels((channels) => {
|
|
183
|
+
channels.push(channel);
|
|
184
|
+
});
|
|
185
|
+
onConnected(connection, meta);
|
|
186
|
+
},
|
|
187
|
+
message: (peer, message) => {
|
|
188
|
+
states.get(peer)?.onMessage?.(message.text());
|
|
189
|
+
},
|
|
190
|
+
close: (peer) => {
|
|
191
|
+
const state = states.get(peer);
|
|
192
|
+
if (!state) return;
|
|
193
|
+
states.delete(peer);
|
|
194
|
+
rpcGroup.updateChannels((channels) => {
|
|
195
|
+
const index = channels.indexOf(state.channel);
|
|
196
|
+
if (index >= 0) channels.splice(index, 1);
|
|
197
|
+
});
|
|
198
|
+
onDisconnected(state.connection, state.meta);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Attach a WebSocket transport to an existing RPC group, powered by
|
|
204
|
+
* [crossws](https://crossws.h3.dev). Either attach to an existing HTTP(S)
|
|
205
|
+
* `server` (sharing its port, optionally scoped to a `path`), or let this
|
|
206
|
+
* helper create a standalone server from `port` / `host` / `https`.
|
|
207
|
+
*
|
|
208
|
+
* Returns the crossws node adapter, standalone-server readiness/address
|
|
209
|
+
* accessors, `detach` (remove the upgrade listener from a shared `server`),
|
|
210
|
+
* and `close` (full deterministic teardown).
|
|
211
|
+
*/
|
|
212
|
+
function attachWsRpcTransport(rpcGroup, options = {}) {
|
|
213
|
+
const { server, port, host = "localhost", path, destroyUnmatched = false, unbound, https, allowedOrigins } = options;
|
|
214
|
+
const ws = crossws({ hooks: createWsRpcPeerHooks(rpcGroup, options) });
|
|
215
|
+
const sharedUpgradeListener = createUpgradeListener(ws, path, destroyUnmatched, allowedOrigins);
|
|
216
|
+
const ownedUpgradeListener = createUpgradeListener(ws, path, true, allowedOrigins);
|
|
217
|
+
/** Bind a server's `upgrade` events, tracked so `close()` detaches them. */
|
|
218
|
+
const attachments = /* @__PURE__ */ new Set();
|
|
219
|
+
function attachTo(target, listener) {
|
|
220
|
+
target.on("upgrade", listener);
|
|
221
|
+
const detachOne = () => {
|
|
222
|
+
target.off("upgrade", listener);
|
|
223
|
+
attachments.delete(detachOne);
|
|
224
|
+
};
|
|
225
|
+
attachments.add(detachOne);
|
|
226
|
+
return detachOne;
|
|
227
|
+
}
|
|
228
|
+
let ready = Promise.resolve();
|
|
229
|
+
let ownedServer;
|
|
230
|
+
if (unbound) {} else if (server) attachTo(server, sharedUpgradeListener);
|
|
231
|
+
else if (https) {
|
|
232
|
+
ownedServer = createServer$1(https);
|
|
233
|
+
attachTo(ownedServer, ownedUpgradeListener);
|
|
234
|
+
ready = listen(ownedServer, port ?? 0, host);
|
|
235
|
+
} else {
|
|
236
|
+
ownedServer = createServer((_req, res) => {
|
|
237
|
+
res.writeHead(426, { "content-type": "text/plain" });
|
|
238
|
+
res.end("Upgrade Required");
|
|
239
|
+
});
|
|
240
|
+
attachTo(ownedServer, ownedUpgradeListener);
|
|
241
|
+
ready = listen(ownedServer, port ?? 0, host);
|
|
242
|
+
}
|
|
243
|
+
const activeServer = server ?? ownedServer;
|
|
244
|
+
function detachAll() {
|
|
245
|
+
for (const detachOne of [...attachments]) detachOne();
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
ws,
|
|
249
|
+
ready,
|
|
250
|
+
address: () => activeServer?.address() ?? null,
|
|
251
|
+
handleUpgrade: sharedUpgradeListener,
|
|
252
|
+
attach: (target) => attachTo(target, sharedUpgradeListener),
|
|
253
|
+
detach: detachAll,
|
|
254
|
+
async close() {
|
|
255
|
+
detachAll();
|
|
256
|
+
ws.closeAll(void 0, void 0, true);
|
|
257
|
+
if (ownedServer) {
|
|
258
|
+
const srv = ownedServer;
|
|
259
|
+
await ready.catch(() => {});
|
|
260
|
+
if (!srv.listening) return;
|
|
261
|
+
await new Promise((r) => srv.close(() => r()));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
export { isLoopbackHostname as a, isAllowedOrigin as i, createWsOriginRegistry as n, createRpcSessionMeta as o, createWsRpcPeerHooks as r, attachWsRpcTransport as t };
|