devframe 0.7.16 → 0.8.1
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/bin/devframe.mjs +8 -0
- package/dist/adapters/build.d.mts +1 -1
- package/dist/adapters/build.mjs +4 -4
- package/dist/adapters/cac.d.mts +1 -1
- package/dist/adapters/cac.mjs +1 -1
- package/dist/adapters/cli.d.mts +1 -1
- package/dist/adapters/cli.mjs +1 -1
- package/dist/adapters/dev.d.mts +2 -2
- package/dist/adapters/dev.mjs +1 -1
- package/dist/adapters/embedded.d.mts +1 -1
- package/dist/adapters/mcp.d.mts +51 -3
- package/dist/adapters/mcp.mjs +2 -2
- package/dist/agent-tool-name-C3b5vEwJ.mjs +34 -0
- package/dist/{cac-CPaOy3LD.mjs → cac-B1LM3zit.mjs} +18 -13
- package/dist/cli/main.d.mts +11 -0
- package/dist/cli/main.mjs +284 -0
- package/dist/client/index.d.mts +40 -11
- package/dist/client/index.mjs +129 -19
- package/dist/constants.d.mts +13 -6
- package/dist/constants.mjs +13 -6
- package/dist/{context-DY72brRI.mjs → context-CQefP2jR.mjs} +3 -2
- package/dist/{context-CzqPrJSz.d.mts → context-DFzmxCLa.d.mts} +1 -1
- package/dist/{revoke-BtQDKTp7.mjs → crypto-token-XCqTSMg9.mjs} +1 -35
- package/dist/{dev-BCeaQuGE.mjs → dev-BSHFZGZr.mjs} +22 -11
- package/dist/{devframe-Ckhf3kFw.d.mts → devframe-qgCKL683.d.mts} +77 -15
- package/dist/diagnostics-B5-qHeqD.mjs +94 -0
- package/dist/{diagnostics-BXwBQmoN.mjs → diagnostics-hwjXp_UV.mjs} +8 -0
- package/dist/{dump-Cz7yVvsB.mjs → dump-CgZShDRB.mjs} +82 -6
- package/dist/fetch-CK0S253E.mjs +534 -0
- package/dist/helpers/vite.d.mts +15 -14
- package/dist/helpers/vite.mjs +8 -8
- package/dist/{host-h3-CgOetlNE.mjs → host-h3-Kz7t5Xab.mjs} +83 -28
- package/dist/http-Dvgxujcu.mjs +38 -0
- package/dist/{index-8dndvIxR.d.mts → index-B7RgLw--.d.mts} +11 -6
- package/dist/{index-DgsLFhZg.d.mts → index-Dbw1p5ch.d.mts} +27 -3
- package/dist/{index-YDSJgsyG.d.mts → index-vAX6bYuc.d.mts} +1 -1
- package/dist/index.d.mts +5 -5
- package/dist/instance-registry-D6fxYu36.mjs +190 -0
- package/dist/node/auth.d.mts +2 -2
- package/dist/node/auth.mjs +2 -2
- package/dist/node/hub-internals.d.mts +2 -2
- package/dist/node/hub-internals.mjs +1 -1
- package/dist/node/index.d.mts +124 -6
- package/dist/node/index.mjs +5 -4
- package/dist/recipes/common-rpc-functions.d.mts +33 -33
- package/dist/recipes/common-rpc-functions.mjs +6 -6
- package/dist/recipes/interactive-auth.d.mts +2 -2
- package/dist/recipes/interactive-auth.mjs +23 -15
- package/dist/revoke-DZbJ7Cl0.mjs +35 -0
- package/dist/rpc/dump.d.mts +1 -1
- package/dist/rpc/dump.mjs +1 -1
- package/dist/rpc/index.d.mts +3 -3
- package/dist/rpc/index.mjs +4 -4
- package/dist/rpc/transports/ws-client.d.mts +1 -1
- package/dist/rpc/transports/ws-client.mjs +1 -1
- package/dist/rpc/transports/ws-server.d.mts +2 -2
- package/dist/rpc/transports/ws-server.mjs +50 -3
- package/dist/{serialization-DpLXCy13.mjs → serialization-C8Mnw9hK.mjs} +1 -1
- package/dist/{server-DykHCza_.mjs → server-CXaDDUl0.mjs} +2 -2
- package/dist/{server-B0wiP_Oi.d.mts → server-E9z4ZWbd.d.mts} +4 -3
- package/dist/simple-schema-BDzLeJDk.d.mts +102 -0
- package/dist/simple-schema-DQPZrAaZ.mjs +154 -0
- package/dist/{state-WX3HT5a3.mjs → state-BeUHDDjk.mjs} +13 -6
- package/dist/{storage-gEnj9ASQ.mjs → storage-Dzoc3NVC.mjs} +2 -66
- package/dist/{transports-BmDVn4SF.mjs → transports-vhizgqXM.mjs} +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-CrzNxXKq.d.mts → types-CnJSgRVa.d.mts} +33 -19
- package/dist/utils/agent-tool-name.d.mts +27 -0
- package/dist/utils/agent-tool-name.mjs +34 -0
- package/dist/utils/events.d.mts +1 -1
- package/dist/utils/shared-state.d.mts +1 -1
- package/dist/utils/simple-schema.d.mts +2 -0
- package/dist/utils/simple-schema.mjs +154 -0
- package/dist/utils/streaming-channel.d.mts +1 -1
- package/dist/{ws-client-DSXiI2h7.d.mts → ws-client-B6Tnr0ic.d.mts} +1 -1
- package/dist/{ws-server-9-wn7MNQ.d.mts → ws-server-J-zkiOyp.d.mts} +24 -3
- package/package.json +18 -7
- package/skills/devframe/SKILL.md +4 -3
- package/dist/build-server-C7vTKjiQ.mjs +0 -359
- package/dist/http-_pPbryTQ.mjs +0 -114
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
//#region src/utils/simple-schema.ts
|
|
2
|
+
function ok(value) {
|
|
3
|
+
return { value };
|
|
4
|
+
}
|
|
5
|
+
function fail(message, path) {
|
|
6
|
+
return { issues: [path ? {
|
|
7
|
+
message,
|
|
8
|
+
path
|
|
9
|
+
} : { message }] };
|
|
10
|
+
}
|
|
11
|
+
function make(type, validate, extra) {
|
|
12
|
+
return {
|
|
13
|
+
type,
|
|
14
|
+
...extra,
|
|
15
|
+
"~standard": {
|
|
16
|
+
version: 1,
|
|
17
|
+
vendor: "devframe",
|
|
18
|
+
validate
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** Run a Standard Schema synchronously, rejecting async validators. */
|
|
23
|
+
function runSync(schema, value) {
|
|
24
|
+
const result = schema["~standard"].validate(value);
|
|
25
|
+
if (result instanceof Promise) throw new TypeError("[devframe/utils/simple-schema] async validators are not supported inside object()/optional()/nullable()");
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
/** Any string. */
|
|
29
|
+
function string() {
|
|
30
|
+
return make("string", (v) => typeof v === "string" ? ok(v) : fail("Expected a string"));
|
|
31
|
+
}
|
|
32
|
+
/** A finite number (rejects `NaN`). */
|
|
33
|
+
function number() {
|
|
34
|
+
return make("number", (v) => typeof v === "number" && !Number.isNaN(v) ? ok(v) : fail("Expected a number"));
|
|
35
|
+
}
|
|
36
|
+
/** A boolean. */
|
|
37
|
+
function boolean() {
|
|
38
|
+
return make("boolean", (v) => typeof v === "boolean" ? ok(v) : fail("Expected a boolean"));
|
|
39
|
+
}
|
|
40
|
+
/** `undefined` — mirrors valibot's `void`. */
|
|
41
|
+
function voidType() {
|
|
42
|
+
return make("void", (v) => v === void 0 ? ok(void 0) : fail("Expected undefined"));
|
|
43
|
+
}
|
|
44
|
+
/** `null`. */
|
|
45
|
+
function nullType() {
|
|
46
|
+
return make("null", (v) => v === null ? ok(null) : fail("Expected null"));
|
|
47
|
+
}
|
|
48
|
+
/** One of a fixed set of literal values. */
|
|
49
|
+
function picklist(values) {
|
|
50
|
+
const set = new Set(values);
|
|
51
|
+
return make("picklist", (v) => set.has(v) ? ok(v) : fail(`Expected one of: ${values.join(", ")}`), { values });
|
|
52
|
+
}
|
|
53
|
+
/** A single literal value (string / number / boolean). */
|
|
54
|
+
function literal(value) {
|
|
55
|
+
return make("literal", (v) => v === value ? ok(v) : fail(`Expected ${JSON.stringify(value)}`), { value });
|
|
56
|
+
}
|
|
57
|
+
/** A value matching any one of the given schemas. */
|
|
58
|
+
function union(options) {
|
|
59
|
+
return make("union", (v) => {
|
|
60
|
+
const issues = [];
|
|
61
|
+
for (const option of options) {
|
|
62
|
+
const result = runSync(option, v);
|
|
63
|
+
if (!result.issues) return ok(v);
|
|
64
|
+
issues.push(...result.issues);
|
|
65
|
+
}
|
|
66
|
+
return { issues };
|
|
67
|
+
}, { options });
|
|
68
|
+
}
|
|
69
|
+
/** A record with string keys whose values each satisfy the value schema. */
|
|
70
|
+
function record(_key, value) {
|
|
71
|
+
return make("record", (v) => {
|
|
72
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) return fail("Expected an object");
|
|
73
|
+
const obj = v;
|
|
74
|
+
const issues = [];
|
|
75
|
+
for (const key of Object.keys(obj)) {
|
|
76
|
+
const result = runSync(value, obj[key]);
|
|
77
|
+
if (result.issues) for (const issue of result.issues) issues.push({
|
|
78
|
+
message: issue.message,
|
|
79
|
+
path: [key, ...issue.path ?? []]
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return issues.length ? { issues } : ok(v);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** An array whose every element satisfies the item schema. */
|
|
86
|
+
function array(item) {
|
|
87
|
+
return make("array", (v) => {
|
|
88
|
+
if (!Array.isArray(v)) return fail("Expected an array");
|
|
89
|
+
const issues = [];
|
|
90
|
+
for (let i = 0; i < v.length; i++) {
|
|
91
|
+
const result = runSync(item, v[i]);
|
|
92
|
+
if (result.issues) for (const issue of result.issues) issues.push({
|
|
93
|
+
message: issue.message,
|
|
94
|
+
path: [i, ...issue.path ?? []]
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return issues.length ? { issues } : ok(v);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/** An object whose known keys each satisfy their schema (extra keys are kept). */
|
|
101
|
+
function object(shape) {
|
|
102
|
+
const entries = Object.entries(shape);
|
|
103
|
+
return make("object", (v) => {
|
|
104
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) return fail("Expected an object");
|
|
105
|
+
const obj = v;
|
|
106
|
+
const issues = [];
|
|
107
|
+
for (const [key, schema] of entries) {
|
|
108
|
+
const result = runSync(schema, obj[key]);
|
|
109
|
+
if (result.issues) for (const issue of result.issues) issues.push({
|
|
110
|
+
message: issue.message,
|
|
111
|
+
path: [key, ...issue.path ?? []]
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return issues.length ? { issues } : ok(v);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** Allow `undefined` in addition to the inner schema. */
|
|
118
|
+
function optional(inner) {
|
|
119
|
+
return make("optional", (v) => v === void 0 ? ok(void 0) : runSync(inner, v), { wrapped: inner });
|
|
120
|
+
}
|
|
121
|
+
/** Allow `null` in addition to the inner schema. */
|
|
122
|
+
function nullable(inner) {
|
|
123
|
+
return make("nullable", (v) => v === null ? ok(null) : runSync(inner, v), { wrapped: inner });
|
|
124
|
+
}
|
|
125
|
+
/** Attach a human-readable description (used for CLI option help). */
|
|
126
|
+
function describe(schema, description) {
|
|
127
|
+
return {
|
|
128
|
+
...schema,
|
|
129
|
+
description
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Grouped access to every builder — `s.string()`, `s.object({ ... })`,
|
|
134
|
+
* `s.void()`, etc. Handy for a valibot-like `import { s } from
|
|
135
|
+
* 'devframe/utils/simple-schema'` call site.
|
|
136
|
+
*/
|
|
137
|
+
const s = {
|
|
138
|
+
string,
|
|
139
|
+
number,
|
|
140
|
+
boolean,
|
|
141
|
+
void: voidType,
|
|
142
|
+
null: nullType,
|
|
143
|
+
literal,
|
|
144
|
+
picklist,
|
|
145
|
+
union,
|
|
146
|
+
record,
|
|
147
|
+
array,
|
|
148
|
+
object,
|
|
149
|
+
optional,
|
|
150
|
+
nullable,
|
|
151
|
+
describe
|
|
152
|
+
};
|
|
153
|
+
//#endregion
|
|
154
|
+
export { array, boolean, describe, literal, nullType, nullable, number, object, optional, picklist, record, s, string, union, voidType };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { at as StreamReader, ct as createStreamReader, it as StreamErrorPayload, lt as createStreamSink, nt as CreateStreamReaderOptions, ot as StreamSink, rt as CreateStreamSinkOptions, st as StreamSinkEvents, tt as BufferedChunk } from "../devframe-
|
|
1
|
+
import { at as StreamReader, ct as createStreamReader, it as StreamErrorPayload, lt as createStreamSink, nt as CreateStreamReaderOptions, ot as StreamSink, rt as CreateStreamSinkOptions, st as StreamSinkEvents, tt as BufferedChunk } from "../devframe-qgCKL683.mjs";
|
|
2
2
|
export { BufferedChunk, CreateStreamReaderOptions, CreateStreamSinkOptions, StreamErrorPayload, StreamReader, StreamSink, StreamSinkEvents, createStreamReader, createStreamSink };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { v as RpcFunctionDefinitionAny } from "./types-
|
|
1
|
+
import { v as RpcFunctionDefinitionAny } from "./types-CnJSgRVa.mjs";
|
|
2
2
|
import { Peer } from "crossws";
|
|
3
3
|
import { BirpcGroup, ChannelOptions } from "birpc";
|
|
4
4
|
import { NodeAdapter } from "crossws/adapters/node";
|
|
@@ -62,7 +62,7 @@ interface WsRpcTransportOptions {
|
|
|
62
62
|
* Pass `false` to disable origin checking entirely (not recommended).
|
|
63
63
|
* Default: loopback-only.
|
|
64
64
|
*/
|
|
65
|
-
allowedOrigins?: readonly string[] | false;
|
|
65
|
+
allowedOrigins?: readonly string[] | WsOriginRegistry | false;
|
|
66
66
|
/**
|
|
67
67
|
* RPC function definitions, used by the per-call wire serializer to
|
|
68
68
|
* dispatch between strict-JSON and structured-clone encoding based
|
|
@@ -79,6 +79,27 @@ interface WsRpcTransportOptions {
|
|
|
79
79
|
/** Override the default per-call deserializer. Most callers should leave this unset. */
|
|
80
80
|
deserialize?: ChannelOptions['deserialize'];
|
|
81
81
|
}
|
|
82
|
+
interface CreateWsOriginRegistryOptions {
|
|
83
|
+
/** Origins allowed before any external viewers are registered. */
|
|
84
|
+
allowedOrigins?: readonly string[];
|
|
85
|
+
/** Additional validation to run after the registration token is verified. */
|
|
86
|
+
validateOrigin?: (origin: string) => boolean;
|
|
87
|
+
}
|
|
88
|
+
interface WsOriginRegistry {
|
|
89
|
+
/** Registration token to include in connection metadata. */
|
|
90
|
+
readonly token: string;
|
|
91
|
+
/** Read and register an origin from a connection bootstrap URL. */
|
|
92
|
+
registerFromUrl: (url: string) => string | undefined;
|
|
93
|
+
/** Check whether an origin is currently allowed. */
|
|
94
|
+
isAllowed: (origin: string | undefined) => boolean;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Create a live, token-protected origin allowlist for external browser
|
|
98
|
+
* viewers. Pass it to {@link WsRpcTransportOptions.allowedOrigins}, then use
|
|
99
|
+
* `registerFromUrl()` in the connection metadata handler to authorize a
|
|
100
|
+
* viewer without sharing a mutable array or disabling DNS-rebinding protection.
|
|
101
|
+
*/
|
|
102
|
+
declare function createWsOriginRegistry(options?: CreateWsOriginRegistryOptions): WsOriginRegistry;
|
|
82
103
|
interface WsRpcTransport {
|
|
83
104
|
/**
|
|
84
105
|
* The crossws node adapter driving the socket — exposes the connected
|
|
@@ -115,4 +136,4 @@ declare function isAllowedOrigin(origin: string | undefined, allowedOrigins: rea
|
|
|
115
136
|
*/
|
|
116
137
|
declare function attachWsRpcTransport<ClientFunctions extends object, ServerFunctions extends object>(rpcGroup: BirpcGroup<ClientFunctions, ServerFunctions, false>, options?: WsRpcTransportOptions): WsRpcTransport;
|
|
117
138
|
//#endregion
|
|
118
|
-
export {
|
|
139
|
+
export { WsRpcTransportOptions as a, isAllowedOrigin as c, WsRpcTransport as i, isLoopbackHostname as l, DevframeNodeRpcSessionMeta as n, attachWsRpcTransport as o, WsOriginRegistry as r, createWsOriginRegistry as s, CreateWsOriginRegistryOptions as t };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devframe",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.1",
|
|
5
5
|
"description": "Framework for building generic devframes",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"./rpc/transports/ws-client": "./dist/rpc/transports/ws-client.mjs",
|
|
43
43
|
"./rpc/transports/ws-server": "./dist/rpc/transports/ws-server.mjs",
|
|
44
44
|
"./types": "./dist/types/index.mjs",
|
|
45
|
+
"./utils/agent-tool-name": "./dist/utils/agent-tool-name.mjs",
|
|
45
46
|
"./utils/colors": "./dist/utils/colors.mjs",
|
|
46
47
|
"./utils/crypto-token": "./dist/utils/crypto-token.mjs",
|
|
47
48
|
"./utils/events": "./dist/utils/events.mjs",
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
"./utils/nanoid": "./dist/utils/nanoid.mjs",
|
|
51
52
|
"./utils/open": "./dist/utils/open.mjs",
|
|
52
53
|
"./utils/promise": "./dist/utils/promise.mjs",
|
|
54
|
+
"./utils/simple-schema": "./dist/utils/simple-schema.mjs",
|
|
53
55
|
"./utils/scope": "./dist/utils/scope.mjs",
|
|
54
56
|
"./utils/serve-static": "./dist/utils/serve-static.mjs",
|
|
55
57
|
"./utils/shared-state": "./dist/utils/shared-state.mjs",
|
|
@@ -59,16 +61,24 @@
|
|
|
59
61
|
"./package.json": "./package.json"
|
|
60
62
|
},
|
|
61
63
|
"types": "./dist/index.d.mts",
|
|
64
|
+
"bin": {
|
|
65
|
+
"devframe": "./bin/devframe.mjs"
|
|
66
|
+
},
|
|
62
67
|
"files": [
|
|
68
|
+
"bin",
|
|
63
69
|
"dist",
|
|
64
70
|
"skills"
|
|
65
71
|
],
|
|
66
72
|
"peerDependencies": {
|
|
67
|
-
"@modelcontextprotocol/
|
|
73
|
+
"@modelcontextprotocol/client": "^2.0.0",
|
|
74
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
68
75
|
"cac": "^7.0.0"
|
|
69
76
|
},
|
|
70
77
|
"peerDependenciesMeta": {
|
|
71
|
-
"@modelcontextprotocol/
|
|
78
|
+
"@modelcontextprotocol/client": {
|
|
79
|
+
"optional": true
|
|
80
|
+
},
|
|
81
|
+
"@modelcontextprotocol/server": {
|
|
72
82
|
"optional": true
|
|
73
83
|
},
|
|
74
84
|
"cac": {
|
|
@@ -76,7 +86,7 @@
|
|
|
76
86
|
}
|
|
77
87
|
},
|
|
78
88
|
"dependencies": {
|
|
79
|
-
"@
|
|
89
|
+
"@standard-schema/spec": "^1.1.0",
|
|
80
90
|
"birpc": "^4.0.0",
|
|
81
91
|
"crossws": "^0.4.10",
|
|
82
92
|
"destr": "^2.0.5",
|
|
@@ -84,11 +94,11 @@
|
|
|
84
94
|
"mrmime": "^2.0.1",
|
|
85
95
|
"nostics": "^1.2.0",
|
|
86
96
|
"pathe": "^2.0.3",
|
|
87
|
-
"ufo": "^1.6.4"
|
|
88
|
-
"valibot": "^1.4.2"
|
|
97
|
+
"ufo": "^1.6.4"
|
|
89
98
|
},
|
|
90
99
|
"devDependencies": {
|
|
91
|
-
"@modelcontextprotocol/
|
|
100
|
+
"@modelcontextprotocol/client": "^2.0.0",
|
|
101
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
92
102
|
"cac": "^7.0.0",
|
|
93
103
|
"get-port-please": "^3.2.0",
|
|
94
104
|
"immer": "^11.1.15",
|
|
@@ -103,6 +113,7 @@
|
|
|
103
113
|
"tinyglobby": "^0.2.17",
|
|
104
114
|
"tsdown": "^0.22.14",
|
|
105
115
|
"ua-parser-modern": "^0.1.1",
|
|
116
|
+
"valibot": "^1.4.2",
|
|
106
117
|
"whenexpr": "^0.1.2",
|
|
107
118
|
"ws": "^8.21.1"
|
|
108
119
|
},
|
package/skills/devframe/SKILL.md
CHANGED
|
@@ -485,7 +485,7 @@ import { createMcpServer } from 'devframe/adapters/mcp'
|
|
|
485
485
|
await createMcpServer(devframe, { transport: 'stdio' })
|
|
486
486
|
```
|
|
487
487
|
|
|
488
|
-
`@modelcontextprotocol/
|
|
488
|
+
`@modelcontextprotocol/server` is a peer dependency. The CLI adapter also exposes `my-devframe mcp` — route host logs to stderr (stdout is the MCP transport). Safety classifications (`'read' | 'action' | 'destructive'`) drive MCP hint annotations that agent clients use to prompt for confirmation.
|
|
489
489
|
|
|
490
490
|
## Author SPA
|
|
491
491
|
|
|
@@ -569,10 +569,11 @@ RPC handlers run with the full privileges of the host process, so the boundary t
|
|
|
569
569
|
- **`auth` defaults to `true`** — dev-mode connections must authenticate before calls are accepted. Devframe ships the node primitives (`exchangeTempAuthCode`, `verifyAuthToken` in `devframe/node/auth`); the host adapter (e.g. Vite DevTools) provides the interactive `devframe:anonymous:auth` + `devframe:auth:exchange` handlers and auth UI.
|
|
570
570
|
- **`auth: false` trusts every reachable connection.** Use it only for single-user `localhost` tools. Never combine it with a non-loopback bind host, a tunnel, or a shared/CI environment. The default bind host is already `localhost`.
|
|
571
571
|
- **Authentication** exchanges a 6-digit one-time code (shown in the developer's terminal) for a node-issued bearer token via `requestTrustWithCode(code)`. The code is single-use, expires in 5 min, compared in constant time, and rotates after repeated failures — show it only in the terminal, never over the network.
|
|
572
|
-
- **Magic-link (optional):** print `buildOtpAuthUrl(origin)` — `<origin
|
|
572
|
+
- **Magic-link (optional):** print `buildOtpAuthUrl(origin)` — `<origin>/#devframe_otp=<code>`. The code rides the URL **fragment**, which the browser never sends to the server, so it stays out of access logs and `Referer`. `connectDevframe` reads the code, exchanges it, and strips it from the URL. Integrations can opt out (`otpParam: false`) and drive it via the exposed `authenticateWithUrlOtp(rpc)` / `consumeOtpFromUrl()` client utilities. Only the single-use code rides the URL, never the bearer; treat the printed link like the code itself. The standalone CLI's `--open` does this automatically via `DevframeAuthHandler.buildOpenUrl` — the launched tab already carries the OTP, no prompt needed.
|
|
573
573
|
- **Tokens are secrets.** The bearer token rides the WS URL (`?devframe_auth_token=…`) — serve over `wss://`/`https://` beyond loopback. Never log the token or code, never bake them into build output. Revoke via `revokeAuthToken(...)`; clients drop to untrusted on `devframe:auth:revoked`.
|
|
574
574
|
- **Authorize handlers.** Any trusted client can call any registered function — validate inputs, and mark state-changing functions `type: 'destructive'` so MCP/agent clients prompt first.
|
|
575
|
-
- **Origin-lock remote docks** (`originLock
|
|
575
|
+
- **Origin-lock remote docks** (`originLock`, on by default) so a dock's session token is honored only on a connection whose `Origin` matches the dock — the connect-time gate enforces it.
|
|
576
|
+
- **The MCP route requires an Origin.** The route-based MCP server (`cli.mcp`, `viteDevBridge`/Next handler `mcp`, `createMcpFetchHandler`) rejects `Origin`-less requests — a request must carry a loopback (or allow-listed) `Origin`, so it isn't reachable by an arbitrary local process. `devframe connect` sends each instance's own loopback origin automatically.
|
|
576
577
|
|
|
577
578
|
See [Security](https://devfra.me/security) for the full reference.
|
|
578
579
|
|
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
import { a as diagnostics } from "./storage-gEnj9ASQ.mjs";
|
|
2
|
-
import { n as createHostContext } from "./host-h3-CgOetlNE.mjs";
|
|
3
|
-
import { Diagnostic } from "nostics";
|
|
4
|
-
import { join } from "pathe";
|
|
5
|
-
import process from "node:process";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8
|
-
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
-
import { toJsonSchema } from "@valibot/to-json-schema";
|
|
10
|
-
//#region src/adapters/mcp/stringify.ts
|
|
11
|
-
/**
|
|
12
|
-
* JSON-coercing serializer for MCP text payloads.
|
|
13
|
-
*
|
|
14
|
-
* MCP carries tool results and resource reads as plain text over a
|
|
15
|
-
* JSON-RPC transport, so we cannot use the `s:`-prefixed structured-clone
|
|
16
|
-
* format the WS RPC transport falls back to for non-JSON values. Instead,
|
|
17
|
-
* we coerce common non-JSON types into JSON-friendly forms so the LLM
|
|
18
|
-
* client sees something useful instead of `[object Object]`.
|
|
19
|
-
*
|
|
20
|
-
* Coercions:
|
|
21
|
-
* - `BigInt` → `"123n"`
|
|
22
|
-
* - `Date` → ISO string (via the native `toJSON`)
|
|
23
|
-
* - `Map` → `{ __type: 'Map', entries: [[k, v], …] }`
|
|
24
|
-
* - `Set` → `{ __type: 'Set', entries: [v, …] }`
|
|
25
|
-
* - `Error` → `{ name, message, stack, cause? }` (cause recurses)
|
|
26
|
-
* - `Function` → `"[Function: name]"`
|
|
27
|
-
* - `Symbol` → `value.toString()`
|
|
28
|
-
* - cycles → `"[Circular]"`
|
|
29
|
-
*/
|
|
30
|
-
function stringifyForMcp(value) {
|
|
31
|
-
if (value === void 0) return "undefined";
|
|
32
|
-
if (typeof value === "string") return value;
|
|
33
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
34
|
-
return JSON.stringify(value, (_key, val) => {
|
|
35
|
-
if (typeof val === "bigint") return `${val}n`;
|
|
36
|
-
if (val instanceof Error) {
|
|
37
|
-
const out = {
|
|
38
|
-
name: val.name,
|
|
39
|
-
message: val.message,
|
|
40
|
-
stack: val.stack
|
|
41
|
-
};
|
|
42
|
-
if (val.cause !== void 0) out.cause = val.cause;
|
|
43
|
-
return out;
|
|
44
|
-
}
|
|
45
|
-
if (val instanceof Map) return {
|
|
46
|
-
__type: "Map",
|
|
47
|
-
entries: [...val.entries()]
|
|
48
|
-
};
|
|
49
|
-
if (val instanceof Set) return {
|
|
50
|
-
__type: "Set",
|
|
51
|
-
entries: [...val]
|
|
52
|
-
};
|
|
53
|
-
if (typeof val === "function") return `[Function: ${val.name || "anonymous"}]`;
|
|
54
|
-
if (typeof val === "symbol") return val.toString();
|
|
55
|
-
if (val !== null && typeof val === "object") {
|
|
56
|
-
if (seen.has(val)) return "[Circular]";
|
|
57
|
-
seen.add(val);
|
|
58
|
-
}
|
|
59
|
-
return val;
|
|
60
|
-
}, 2);
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Format a thrown value for an MCP `isError` text payload.
|
|
64
|
-
*
|
|
65
|
-
* A nostics `Diagnostic` (every coded devframe error) becomes structured
|
|
66
|
-
* JSON — `{ error: { code, message, fix?, docs? } }` — so an agent receives
|
|
67
|
-
* the actionable next step (`fix`) and the docs URL instead of a bare
|
|
68
|
-
* message string. Other errors surface `Error.name`/`message`, plus one
|
|
69
|
-
* level of `cause.message` so context isn't dropped silently.
|
|
70
|
-
*/
|
|
71
|
-
function formatMcpError(error) {
|
|
72
|
-
if (error instanceof Diagnostic) return JSON.stringify({ error: {
|
|
73
|
-
code: error.code,
|
|
74
|
-
message: error.message,
|
|
75
|
-
...error.fix ? { fix: error.fix } : {},
|
|
76
|
-
...error.docs ? { docs: error.docs } : {}
|
|
77
|
-
} }, null, 2);
|
|
78
|
-
if (!(error instanceof Error)) return String(error);
|
|
79
|
-
const cause = error.cause;
|
|
80
|
-
const causeText = cause instanceof Error ? ` (cause: ${cause.message})` : cause !== void 0 ? ` (cause: ${String(cause)})` : "";
|
|
81
|
-
return `${error.name}: ${error.message}${causeText}`;
|
|
82
|
-
}
|
|
83
|
-
//#endregion
|
|
84
|
-
//#region src/adapters/mcp/to-json-schema.ts
|
|
85
|
-
const FALLBACK_OBJECT_SCHEMA = Object.freeze({
|
|
86
|
-
type: "object",
|
|
87
|
-
additionalProperties: true
|
|
88
|
-
});
|
|
89
|
-
/**
|
|
90
|
-
* Convert a valibot return schema to JSON Schema.
|
|
91
|
-
* @internal
|
|
92
|
-
*/
|
|
93
|
-
function valibotReturnToJsonSchema(schema) {
|
|
94
|
-
if (!schema) return void 0;
|
|
95
|
-
try {
|
|
96
|
-
return toJsonSchema(schema);
|
|
97
|
-
} catch {
|
|
98
|
-
return FALLBACK_OBJECT_SCHEMA;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Convert positional RPC args schemas to a single MCP-friendly object
|
|
103
|
-
* schema. When the RPC declares `args: [v.object(...)]`, unwrap the
|
|
104
|
-
* single-object schema directly (nicer agent UX than `{ arg0: {...} }`).
|
|
105
|
-
*
|
|
106
|
-
* Returns `undefined` when there are no args (the MCP SDK treats this
|
|
107
|
-
* as `{ type: 'object', properties: {} }`).
|
|
108
|
-
* @internal
|
|
109
|
-
*/
|
|
110
|
-
function valibotArgsToJsonSchema(args) {
|
|
111
|
-
if (!args || args.length === 0) return {
|
|
112
|
-
schema: {
|
|
113
|
-
type: "object",
|
|
114
|
-
properties: {}
|
|
115
|
-
},
|
|
116
|
-
unwrapped: false
|
|
117
|
-
};
|
|
118
|
-
if (args.length === 1) {
|
|
119
|
-
const inner = safeToJsonSchema(args[0]);
|
|
120
|
-
if (isObjectJsonSchema(inner)) return {
|
|
121
|
-
schema: inner,
|
|
122
|
-
unwrapped: true
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
const properties = {};
|
|
126
|
-
const required = [];
|
|
127
|
-
for (let i = 0; i < args.length; i++) {
|
|
128
|
-
const key = `arg${i}`;
|
|
129
|
-
properties[key] = safeToJsonSchema(args[i]);
|
|
130
|
-
required.push(key);
|
|
131
|
-
}
|
|
132
|
-
return {
|
|
133
|
-
schema: {
|
|
134
|
-
type: "object",
|
|
135
|
-
properties,
|
|
136
|
-
required,
|
|
137
|
-
additionalProperties: false
|
|
138
|
-
},
|
|
139
|
-
unwrapped: false
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
function safeToJsonSchema(schema) {
|
|
143
|
-
try {
|
|
144
|
-
return toJsonSchema(schema);
|
|
145
|
-
} catch {
|
|
146
|
-
return FALLBACK_OBJECT_SCHEMA;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
function isObjectJsonSchema(value) {
|
|
150
|
-
return !!value && typeof value === "object" && value.type === "object";
|
|
151
|
-
}
|
|
152
|
-
//#endregion
|
|
153
|
-
//#region src/adapters/mcp/build-server.ts
|
|
154
|
-
/**
|
|
155
|
-
* Wire an MCP {@link Server} to a devframe context. Returns the server
|
|
156
|
-
* plus a disposal function for the subscriptions it sets up. The
|
|
157
|
-
* transport is the caller's responsibility — `createMcpServer` connects
|
|
158
|
-
* stdio; tests can connect an {@link InMemoryTransport} instead.
|
|
159
|
-
*
|
|
160
|
-
* @internal
|
|
161
|
-
*/
|
|
162
|
-
function buildMcpServerFromContext(ctx, options) {
|
|
163
|
-
const server = new Server({
|
|
164
|
-
name: options.serverName,
|
|
165
|
-
version: options.serverVersion
|
|
166
|
-
}, { capabilities: {
|
|
167
|
-
tools: { listChanged: true },
|
|
168
|
-
resources: { listChanged: true }
|
|
169
|
-
} });
|
|
170
|
-
registerToolHandlers(server, ctx);
|
|
171
|
-
registerResourceHandlers(server, ctx, options.exposeSharedState);
|
|
172
|
-
const notify = (method) => {
|
|
173
|
-
server.notification({ method }).catch(() => {});
|
|
174
|
-
};
|
|
175
|
-
const offManifest = ctx.agent.events.on("agent:manifest:changed", () => {
|
|
176
|
-
notify("notifications/tools/list_changed");
|
|
177
|
-
notify("notifications/resources/list_changed");
|
|
178
|
-
});
|
|
179
|
-
const offKeyAdded = ctx.rpc.sharedState.onKeyAdded(() => {
|
|
180
|
-
notify("notifications/resources/list_changed");
|
|
181
|
-
});
|
|
182
|
-
return {
|
|
183
|
-
server,
|
|
184
|
-
dispose: () => {
|
|
185
|
-
offManifest();
|
|
186
|
-
offKeyAdded();
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Build an MCP server over the agent surface of a devframe definition.
|
|
192
|
-
* Currently supports `stdio` transport only.
|
|
193
|
-
*
|
|
194
|
-
* @experimental The agent-native surface is experimental and may change
|
|
195
|
-
* without a major version bump until it stabilizes.
|
|
196
|
-
*/
|
|
197
|
-
async function createMcpServer(definition, options = {}) {
|
|
198
|
-
const transport = options.transport ?? "stdio";
|
|
199
|
-
if (transport !== "stdio") throw diagnostics.DF0017({
|
|
200
|
-
transport,
|
|
201
|
-
reason: "Only stdio transport is supported in this release."
|
|
202
|
-
});
|
|
203
|
-
const ctx = await createHostContext({
|
|
204
|
-
cwd: process.cwd(),
|
|
205
|
-
mode: "dev",
|
|
206
|
-
host: {
|
|
207
|
-
mountStatic: () => {},
|
|
208
|
-
resolveOrigin: () => "mcp://devframe",
|
|
209
|
-
getStorageDir: (scope) => {
|
|
210
|
-
if (scope === "workspace") return join(process.cwd(), ".devframe");
|
|
211
|
-
if (scope === "project") return join(process.cwd(), `node_modules/.${definition.id}/devframe`);
|
|
212
|
-
return join(homedir(), `.${definition.id}/devframe`);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
await definition.setup(ctx);
|
|
217
|
-
const { server, dispose } = buildMcpServerFromContext(ctx, {
|
|
218
|
-
serverName: options.serverName ?? `${definition.id} (devframe)`,
|
|
219
|
-
serverVersion: options.serverVersion ?? definition.version ?? "0.0.0",
|
|
220
|
-
exposeSharedState: options.exposeSharedState ?? true
|
|
221
|
-
});
|
|
222
|
-
const { startStdioTransport } = await import("./transports-BmDVn4SF.mjs");
|
|
223
|
-
let stop;
|
|
224
|
-
try {
|
|
225
|
-
stop = await startStdioTransport(server);
|
|
226
|
-
} catch (error) {
|
|
227
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
228
|
-
throw diagnostics.DF0017({
|
|
229
|
-
transport,
|
|
230
|
-
reason,
|
|
231
|
-
cause: error
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
options.onReady?.({ transport: "stdio" });
|
|
235
|
-
return { async stop() {
|
|
236
|
-
dispose();
|
|
237
|
-
await stop();
|
|
238
|
-
} };
|
|
239
|
-
}
|
|
240
|
-
function registerToolHandlers(server, ctx) {
|
|
241
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
242
|
-
return { tools: ctx.agent.list().tools.map((tool) => projectTool(tool, ctx)) };
|
|
243
|
-
});
|
|
244
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
245
|
-
const { name, arguments: args } = request.params;
|
|
246
|
-
try {
|
|
247
|
-
const tool = ctx.agent.getTool(name);
|
|
248
|
-
const outputSchema = tool ? tool.outputSchema ?? computeOutputSchema(tool, ctx) : void 0;
|
|
249
|
-
const result = await ctx.agent.invoke(name, args ?? {});
|
|
250
|
-
return {
|
|
251
|
-
content: [{
|
|
252
|
-
type: "text",
|
|
253
|
-
text: stringifyForMcp(result)
|
|
254
|
-
}],
|
|
255
|
-
...outputSchema ? { structuredContent: result } : {}
|
|
256
|
-
};
|
|
257
|
-
} catch (error) {
|
|
258
|
-
return {
|
|
259
|
-
isError: true,
|
|
260
|
-
content: [{
|
|
261
|
-
type: "text",
|
|
262
|
-
text: `Error invoking "${name}": ${formatMcpError(error)}`
|
|
263
|
-
}]
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
function registerResourceHandlers(server, ctx, exposeSharedState) {
|
|
269
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
270
|
-
const resources = ctx.agent.list().resources.map((resource) => ({
|
|
271
|
-
uri: resource.uri,
|
|
272
|
-
name: resource.name,
|
|
273
|
-
description: resource.description,
|
|
274
|
-
mimeType: resource.mimeType
|
|
275
|
-
}));
|
|
276
|
-
if (exposeSharedState !== false) {
|
|
277
|
-
const filter = typeof exposeSharedState === "function" ? exposeSharedState : () => true;
|
|
278
|
-
for (const key of ctx.rpc.sharedState.keys()) {
|
|
279
|
-
if (!filter(key)) continue;
|
|
280
|
-
resources.push({
|
|
281
|
-
uri: `devframe://state/${encodeURIComponent(key)}`,
|
|
282
|
-
name: key,
|
|
283
|
-
description: `Shared state: ${key}`,
|
|
284
|
-
mimeType: "application/json"
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return { resources };
|
|
289
|
-
});
|
|
290
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
291
|
-
const { uri } = request.params;
|
|
292
|
-
const parsed = parseResourceUri(uri);
|
|
293
|
-
if (parsed.kind === "resource") {
|
|
294
|
-
const content = await ctx.agent.read(parsed.id);
|
|
295
|
-
return { contents: [{
|
|
296
|
-
uri,
|
|
297
|
-
mimeType: content.mimeType ?? "application/json",
|
|
298
|
-
text: content.text ?? stringifyForMcp(content.json)
|
|
299
|
-
}] };
|
|
300
|
-
}
|
|
301
|
-
if (parsed.kind === "state") return { contents: [{
|
|
302
|
-
uri,
|
|
303
|
-
mimeType: "application/json",
|
|
304
|
-
text: stringifyForMcp((await ctx.rpc.sharedState.get(parsed.key)).value())
|
|
305
|
-
}] };
|
|
306
|
-
throw new Error(`[devframe/mcp] unknown resource URI "${uri}"`);
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
function projectTool(tool, ctx) {
|
|
310
|
-
const inputSchema = tool.inputSchema ?? computeInputSchema(tool, ctx);
|
|
311
|
-
const outputSchema = tool.outputSchema ?? computeOutputSchema(tool, ctx);
|
|
312
|
-
return {
|
|
313
|
-
name: tool.id,
|
|
314
|
-
title: tool.title,
|
|
315
|
-
description: tool.description,
|
|
316
|
-
inputSchema,
|
|
317
|
-
...outputSchema ? { outputSchema } : {},
|
|
318
|
-
annotations: {
|
|
319
|
-
title: tool.title,
|
|
320
|
-
readOnlyHint: tool.safety === "read",
|
|
321
|
-
destructiveHint: tool.safety === "destructive"
|
|
322
|
-
}
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
function computeInputSchema(tool, ctx) {
|
|
326
|
-
if (tool.kind !== "rpc" || !tool.rpcName) return {
|
|
327
|
-
type: "object",
|
|
328
|
-
properties: {}
|
|
329
|
-
};
|
|
330
|
-
const def = ctx.rpc.definitions.get(tool.rpcName);
|
|
331
|
-
if (!def) return {
|
|
332
|
-
type: "object",
|
|
333
|
-
properties: {}
|
|
334
|
-
};
|
|
335
|
-
const args = def.args;
|
|
336
|
-
return valibotArgsToJsonSchema(args).schema;
|
|
337
|
-
}
|
|
338
|
-
function computeOutputSchema(tool, ctx) {
|
|
339
|
-
if (tool.kind !== "rpc" || !tool.rpcName) return void 0;
|
|
340
|
-
const def = ctx.rpc.definitions.get(tool.rpcName);
|
|
341
|
-
if (!def) return void 0;
|
|
342
|
-
return valibotReturnToJsonSchema(def.returns);
|
|
343
|
-
}
|
|
344
|
-
function parseResourceUri(uri) {
|
|
345
|
-
const match = uri.match(/^devframe:\/\/(resource|state)\/(.+)$/);
|
|
346
|
-
if (!match) return { kind: "unknown" };
|
|
347
|
-
const [, kind, rest] = match;
|
|
348
|
-
const decoded = decodeURIComponent(rest);
|
|
349
|
-
if (kind === "resource") return {
|
|
350
|
-
kind: "resource",
|
|
351
|
-
id: decoded
|
|
352
|
-
};
|
|
353
|
-
return {
|
|
354
|
-
kind: "state",
|
|
355
|
-
key: decoded
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
//#endregion
|
|
359
|
-
export { createMcpServer as n, buildMcpServerFromContext as t };
|