libp2p-mesh 2026.6.8 → 2026.6.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.
@@ -1,5 +1,6 @@
1
1
  export declare const MULTIADDR_PATTERN: RegExp;
2
2
  export declare function getDefaultConfig(): Record<string, unknown>;
3
+ export declare function validatePluginConfig(pluginConfig: Record<string, unknown>): void;
3
4
  export declare function resolveConfigPath(): string;
4
5
  export declare function readFullConfig(configPath: string): {
5
6
  config: Record<string, unknown>;
@@ -22,6 +22,107 @@ export function getDefaultConfig() {
22
22
  deliveryAckTimeoutMs: 15000,
23
23
  };
24
24
  }
25
+ const PLUGIN_CONFIG_SCHEMA = {
26
+ type: "object",
27
+ additionalProperties: false,
28
+ properties: {
29
+ listenAddrs: { type: "array", items: { type: "string" } },
30
+ enableWebSocket: { type: "boolean" },
31
+ discovery: { type: "string", enum: ["mdns", "bootstrap", "dht"] },
32
+ bootstrapList: { type: "array", items: { type: "string" } },
33
+ meshTopic: { type: "string" },
34
+ enablePubsub: { type: "boolean" },
35
+ enableAgentSync: { type: "boolean" },
36
+ enableDHT: { type: "boolean" },
37
+ instanceName: { type: "string" },
38
+ enableNATTraversal: { type: "boolean" },
39
+ enableIdentify: { type: "boolean" },
40
+ enableAutoNAT: { type: "boolean" },
41
+ enableUPnP: { type: "boolean" },
42
+ enableCircuitRelay: { type: "boolean" },
43
+ enableCircuitRelayServer: { type: "boolean" },
44
+ enableDCUtR: { type: "boolean" },
45
+ relayList: { type: "array", items: { type: "string" } },
46
+ relayChannel: { type: "string" },
47
+ relayAccountId: { type: "string" },
48
+ discoverRelays: { type: "number" },
49
+ announceAddrs: { type: "array", items: { type: "string" } },
50
+ inboundChannel: { type: "string" },
51
+ inboundTarget: { type: "string" },
52
+ inboundTargets: {
53
+ type: "array",
54
+ items: {
55
+ type: "object",
56
+ additionalProperties: false,
57
+ properties: {
58
+ id: { type: "string" },
59
+ channel: { type: "string" },
60
+ target: { type: "string" },
61
+ },
62
+ required: ["channel", "target"],
63
+ },
64
+ },
65
+ deliveryAckTimeoutMs: { type: "number" },
66
+ },
67
+ };
68
+ function validateSchemaValue(value, schema, pathName) {
69
+ const errors = [];
70
+ if (value === undefined)
71
+ return errors;
72
+ if (schema.type === "object") {
73
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
74
+ return [`${pathName}: expected object`];
75
+ }
76
+ const obj = value;
77
+ const properties = schema.properties ?? {};
78
+ for (const requiredKey of schema.required ?? []) {
79
+ if (obj[requiredKey] === undefined) {
80
+ errors.push(`${pathName}.${requiredKey}: required property is missing`);
81
+ }
82
+ }
83
+ if (schema.additionalProperties === false) {
84
+ for (const key of Object.keys(obj)) {
85
+ if (!properties[key]) {
86
+ errors.push(`${pathName}: must not have additional property "${key}"`);
87
+ }
88
+ }
89
+ }
90
+ for (const [key, childSchema] of Object.entries(properties)) {
91
+ errors.push(...validateSchemaValue(obj[key], childSchema, `${pathName}.${key}`));
92
+ }
93
+ return errors;
94
+ }
95
+ if (schema.type === "array") {
96
+ if (!Array.isArray(value)) {
97
+ return [`${pathName}: expected array`];
98
+ }
99
+ if (schema.items) {
100
+ value.forEach((item, index) => {
101
+ errors.push(...validateSchemaValue(item, schema.items, `${pathName}[${index}]`));
102
+ });
103
+ }
104
+ return errors;
105
+ }
106
+ if (schema.type === "string" && typeof value !== "string") {
107
+ return [`${pathName}: expected string`];
108
+ }
109
+ if (schema.type === "boolean" && typeof value !== "boolean") {
110
+ return [`${pathName}: expected boolean`];
111
+ }
112
+ if (schema.type === "number" && (typeof value !== "number" || Number.isNaN(value))) {
113
+ return [`${pathName}: expected number`];
114
+ }
115
+ if (schema.enum && typeof value === "string" && !schema.enum.includes(value)) {
116
+ errors.push(`${pathName}: must be one of ${schema.enum.join(", ")}`);
117
+ }
118
+ return errors;
119
+ }
120
+ export function validatePluginConfig(pluginConfig) {
121
+ const errors = validateSchemaValue(pluginConfig, PLUGIN_CONFIG_SCHEMA, "plugins.entries.libp2p-mesh.config");
122
+ if (errors.length > 0) {
123
+ throw new Error(`libp2p-mesh 配置无效:\n - ${errors.join("\n - ")}`);
124
+ }
125
+ }
25
126
  export function resolveConfigPath() {
26
127
  if (process.env.OPENCLAW_CONFIG_PATH) {
27
128
  const resolved = process.env.OPENCLAW_CONFIG_PATH.replace(/^~(?=$|\/|\\)/, os.homedir());
@@ -72,15 +173,6 @@ export function writeFullConfig(configPath, pluginConfigUpdates) {
72
173
  throw new Error(`无法读取 ${configPath}: ${err.message}`);
73
174
  }
74
175
  }
75
- // Create backup
76
- try {
77
- if (fs.existsSync(configPath)) {
78
- fs.copyFileSync(configPath, configPath + ".bak");
79
- }
80
- }
81
- catch {
82
- console.warn("备份 openclaw.json 失败,继续写入。");
83
- }
84
176
  // Build output object with deep merge
85
177
  const output = structuredClone(typeof base === "object" && !Array.isArray(base) ? base : {});
86
178
  // Ensure plugins.entries["libp2p-mesh"] exists
@@ -105,20 +197,31 @@ export function writeFullConfig(configPath, pluginConfigUpdates) {
105
197
  }
106
198
  const existing = meshEntry.config;
107
199
  meshEntry.config = { ...existing, ...pluginConfigUpdates };
108
- // Ensure channels["libp2p-mesh"].enabled exists
109
- if (!output.channels ||
110
- typeof output.channels !== "object" ||
111
- Array.isArray(output.channels)) {
112
- output.channels = {};
113
- }
200
+ // Older wizard versions wrote channels["libp2p-mesh"].enabled, but the
201
+ // channel schema does not allow that field. Clean it up when saving.
114
202
  const channels = output.channels;
115
- if (!channels["libp2p-mesh"] ||
116
- typeof channels["libp2p-mesh"] !== "object" ||
117
- Array.isArray(channels["libp2p-mesh"])) {
118
- channels["libp2p-mesh"] = {};
203
+ if (channels && typeof channels === "object" && !Array.isArray(channels)) {
204
+ const channelMap = channels;
205
+ const meshChannel = channelMap["libp2p-mesh"];
206
+ if (meshChannel &&
207
+ typeof meshChannel === "object" &&
208
+ !Array.isArray(meshChannel)) {
209
+ delete meshChannel.enabled;
210
+ if (Object.keys(meshChannel).length === 0) {
211
+ delete channelMap["libp2p-mesh"];
212
+ }
213
+ }
214
+ }
215
+ validatePluginConfig(meshEntry.config);
216
+ // Create backup after validation so invalid config never mutates files.
217
+ try {
218
+ if (fs.existsSync(configPath)) {
219
+ fs.copyFileSync(configPath, configPath + ".bak");
220
+ }
221
+ }
222
+ catch {
223
+ console.warn("备份 openclaw.json 失败,继续写入。");
119
224
  }
120
- const meshChannel = channels["libp2p-mesh"];
121
- meshChannel.enabled = true;
122
225
  // Write atomically (write to temp, then rename)
123
226
  const tmpPath = configPath + ".tmp";
124
227
  try {
@@ -95,7 +95,6 @@ function interactiveSelect(prompt, choices) {
95
95
  return new Promise((resolve) => {
96
96
  const wasRaw = process.stdin.isRaw;
97
97
  const wasPaused = process.stdin.isPaused();
98
- let resolved = false;
99
98
  const onKeypress = (_str, key) => {
100
99
  if (!key || !key.name)
101
100
  return;
@@ -109,7 +108,6 @@ function interactiveSelect(prompt, choices) {
109
108
  reRenderChoices(choices, selectedIdx);
110
109
  }
111
110
  else if (key.name === "return" || key.name === "space") {
112
- resolved = true;
113
111
  const chosen = choices[selectedIdx];
114
112
  cleanup();
115
113
  eraseChoices(choices.length);
@@ -123,8 +121,6 @@ function interactiveSelect(prompt, choices) {
123
121
  }
124
122
  };
125
123
  const cleanup = () => {
126
- if (resolved)
127
- return;
128
124
  try {
129
125
  // Always restore to non-raw so subsequent readline works
130
126
  process.stdin.setRawMode(false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2p-mesh",
3
- "version": "2026.6.8",
3
+ "version": "2026.6.10",
4
4
  "description": "OpenClaw libp2p P2P mesh network plugin for cross-instance agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/config-io.ts CHANGED
@@ -25,6 +25,121 @@ export function getDefaultConfig(): Record<string, unknown> {
25
25
  };
26
26
  }
27
27
 
28
+ type JsonSchema = {
29
+ type?: "object" | "array" | "string" | "boolean" | "number";
30
+ enum?: string[];
31
+ additionalProperties?: boolean;
32
+ properties?: Record<string, JsonSchema>;
33
+ items?: JsonSchema;
34
+ required?: string[];
35
+ };
36
+
37
+ const PLUGIN_CONFIG_SCHEMA: JsonSchema = {
38
+ type: "object",
39
+ additionalProperties: false,
40
+ properties: {
41
+ listenAddrs: { type: "array", items: { type: "string" } },
42
+ enableWebSocket: { type: "boolean" },
43
+ discovery: { type: "string", enum: ["mdns", "bootstrap", "dht"] },
44
+ bootstrapList: { type: "array", items: { type: "string" } },
45
+ meshTopic: { type: "string" },
46
+ enablePubsub: { type: "boolean" },
47
+ enableAgentSync: { type: "boolean" },
48
+ enableDHT: { type: "boolean" },
49
+ instanceName: { type: "string" },
50
+ enableNATTraversal: { type: "boolean" },
51
+ enableIdentify: { type: "boolean" },
52
+ enableAutoNAT: { type: "boolean" },
53
+ enableUPnP: { type: "boolean" },
54
+ enableCircuitRelay: { type: "boolean" },
55
+ enableCircuitRelayServer: { type: "boolean" },
56
+ enableDCUtR: { type: "boolean" },
57
+ relayList: { type: "array", items: { type: "string" } },
58
+ relayChannel: { type: "string" },
59
+ relayAccountId: { type: "string" },
60
+ discoverRelays: { type: "number" },
61
+ announceAddrs: { type: "array", items: { type: "string" } },
62
+ inboundChannel: { type: "string" },
63
+ inboundTarget: { type: "string" },
64
+ inboundTargets: {
65
+ type: "array",
66
+ items: {
67
+ type: "object",
68
+ additionalProperties: false,
69
+ properties: {
70
+ id: { type: "string" },
71
+ channel: { type: "string" },
72
+ target: { type: "string" },
73
+ },
74
+ required: ["channel", "target"],
75
+ },
76
+ },
77
+ deliveryAckTimeoutMs: { type: "number" },
78
+ },
79
+ };
80
+
81
+ function validateSchemaValue(value: unknown, schema: JsonSchema, pathName: string): string[] {
82
+ const errors: string[] = [];
83
+ if (value === undefined) return errors;
84
+
85
+ if (schema.type === "object") {
86
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
87
+ return [`${pathName}: expected object`];
88
+ }
89
+ const obj = value as Record<string, unknown>;
90
+ const properties = schema.properties ?? {};
91
+ for (const requiredKey of schema.required ?? []) {
92
+ if (obj[requiredKey] === undefined) {
93
+ errors.push(`${pathName}.${requiredKey}: required property is missing`);
94
+ }
95
+ }
96
+ if (schema.additionalProperties === false) {
97
+ for (const key of Object.keys(obj)) {
98
+ if (!properties[key]) {
99
+ errors.push(`${pathName}: must not have additional property "${key}"`);
100
+ }
101
+ }
102
+ }
103
+ for (const [key, childSchema] of Object.entries(properties)) {
104
+ errors.push(...validateSchemaValue(obj[key], childSchema, `${pathName}.${key}`));
105
+ }
106
+ return errors;
107
+ }
108
+
109
+ if (schema.type === "array") {
110
+ if (!Array.isArray(value)) {
111
+ return [`${pathName}: expected array`];
112
+ }
113
+ if (schema.items) {
114
+ value.forEach((item, index) => {
115
+ errors.push(...validateSchemaValue(item, schema.items!, `${pathName}[${index}]`));
116
+ });
117
+ }
118
+ return errors;
119
+ }
120
+
121
+ if (schema.type === "string" && typeof value !== "string") {
122
+ return [`${pathName}: expected string`];
123
+ }
124
+ if (schema.type === "boolean" && typeof value !== "boolean") {
125
+ return [`${pathName}: expected boolean`];
126
+ }
127
+ if (schema.type === "number" && (typeof value !== "number" || Number.isNaN(value))) {
128
+ return [`${pathName}: expected number`];
129
+ }
130
+ if (schema.enum && typeof value === "string" && !schema.enum.includes(value)) {
131
+ errors.push(`${pathName}: must be one of ${schema.enum.join(", ")}`);
132
+ }
133
+ return errors;
134
+ }
135
+
136
+ export function validatePluginConfig(pluginConfig: Record<string, unknown>): void {
137
+ const errors = validateSchemaValue(pluginConfig, PLUGIN_CONFIG_SCHEMA, "plugins.entries.libp2p-mesh.config");
138
+ if (errors.length > 0) {
139
+ throw new Error(`libp2p-mesh 配置无效:\n - ${errors.join("\n - ")}`);
140
+ }
141
+ }
142
+
28
143
  export function resolveConfigPath(): string {
29
144
  if (process.env.OPENCLAW_CONFIG_PATH) {
30
145
  const resolved = process.env.OPENCLAW_CONFIG_PATH.replace(/^~(?=$|\/|\\)/, os.homedir());
@@ -92,15 +207,6 @@ export function writeFullConfig(
92
207
  }
93
208
  }
94
209
 
95
- // Create backup
96
- try {
97
- if (fs.existsSync(configPath)) {
98
- fs.copyFileSync(configPath, configPath + ".bak");
99
- }
100
- } catch {
101
- console.warn("备份 openclaw.json 失败,继续写入。");
102
- }
103
-
104
210
  // Build output object with deep merge
105
211
  const output = structuredClone(
106
212
  typeof base === "object" && !Array.isArray(base) ? base : {},
@@ -132,24 +238,34 @@ export function writeFullConfig(
132
238
  const existing = meshEntry.config as Record<string, unknown>;
133
239
  meshEntry.config = { ...existing, ...pluginConfigUpdates };
134
240
 
135
- // Ensure channels["libp2p-mesh"].enabled exists
136
- if (
137
- !output.channels ||
138
- typeof output.channels !== "object" ||
139
- Array.isArray(output.channels)
140
- ) {
141
- output.channels = {};
241
+ // Older wizard versions wrote channels["libp2p-mesh"].enabled, but the
242
+ // channel schema does not allow that field. Clean it up when saving.
243
+ const channels = output.channels;
244
+ if (channels && typeof channels === "object" && !Array.isArray(channels)) {
245
+ const channelMap = channels as Record<string, unknown>;
246
+ const meshChannel = channelMap["libp2p-mesh"];
247
+ if (
248
+ meshChannel &&
249
+ typeof meshChannel === "object" &&
250
+ !Array.isArray(meshChannel)
251
+ ) {
252
+ delete (meshChannel as Record<string, unknown>).enabled;
253
+ if (Object.keys(meshChannel).length === 0) {
254
+ delete channelMap["libp2p-mesh"];
255
+ }
256
+ }
142
257
  }
143
- const channels = output.channels as Record<string, unknown>;
144
- if (
145
- !channels["libp2p-mesh"] ||
146
- typeof channels["libp2p-mesh"] !== "object" ||
147
- Array.isArray(channels["libp2p-mesh"])
148
- ) {
149
- channels["libp2p-mesh"] = {};
258
+
259
+ validatePluginConfig(meshEntry.config as Record<string, unknown>);
260
+
261
+ // Create backup after validation so invalid config never mutates files.
262
+ try {
263
+ if (fs.existsSync(configPath)) {
264
+ fs.copyFileSync(configPath, configPath + ".bak");
265
+ }
266
+ } catch {
267
+ console.warn("备份 openclaw.json 失败,继续写入。");
150
268
  }
151
- const meshChannel = channels["libp2p-mesh"] as Record<string, unknown>;
152
- meshChannel.enabled = true;
153
269
 
154
270
  // Write atomically (write to temp, then rename)
155
271
  const tmpPath = configPath + ".tmp";
package/src/wizard.ts CHANGED
@@ -149,8 +149,6 @@ function interactiveSelect(
149
149
  const wasRaw = process.stdin.isRaw;
150
150
  const wasPaused = process.stdin.isPaused();
151
151
 
152
- let resolved = false;
153
-
154
152
  const onKeypress = (
155
153
  _str: string | undefined,
156
154
  key: { name?: string; ctrl?: boolean },
@@ -165,7 +163,6 @@ function interactiveSelect(
165
163
  selectedIdx = (selectedIdx + 1) % choices.length;
166
164
  reRenderChoices(choices, selectedIdx);
167
165
  } else if (key.name === "return" || key.name === "space") {
168
- resolved = true;
169
166
  const chosen = choices[selectedIdx]!;
170
167
  cleanup();
171
168
  eraseChoices(choices.length);
@@ -179,7 +176,6 @@ function interactiveSelect(
179
176
  };
180
177
 
181
178
  const cleanup = () => {
182
- if (resolved) return;
183
179
  try {
184
180
  // Always restore to non-raw so subsequent readline works
185
181
  process.stdin.setRawMode(false);