rol-websocket-channel 1.4.9 → 1.5.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/index.js CHANGED
@@ -12,6 +12,21 @@ let pluginRuntime = null;
12
12
  export function getPluginRuntime() {
13
13
  return pluginRuntime;
14
14
  }
15
+ export function formatCliErrorPayload(error) {
16
+ const data = error?.data;
17
+ const dataObject = data && typeof data === "object" && !Array.isArray(data)
18
+ ? data
19
+ : {};
20
+ const code = dataObject.code ?? error?.code;
21
+ return {
22
+ ok: false,
23
+ error: {
24
+ message: error instanceof Error ? error.message : String(error),
25
+ ...dataObject,
26
+ ...(code === undefined ? {} : { code }),
27
+ },
28
+ };
29
+ }
15
30
  // ============================================
16
31
  // 4. 插件主体定义
17
32
  // ============================================
@@ -554,13 +569,7 @@ function registerAdminBridgeCli(api) {
554
569
  }
555
570
  catch (error) {
556
571
  process.exitCode = 1;
557
- process.stderr.write(JSON.stringify({
558
- ok: false,
559
- error: {
560
- message: error instanceof Error ? error.message : String(error),
561
- code: error?.data?.code,
562
- },
563
- }, null, 2) + "\n");
572
+ process.stderr.write(JSON.stringify(formatCliErrorPayload(error), null, 2) + "\n");
564
573
  }
565
574
  });
566
575
  const mem9 = root
@@ -1,7 +1,7 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import test from 'node:test';
4
- import register from '../../index.js';
4
+ import register, { formatCliErrorPayload } from '../../index.js';
5
5
  const manifest = JSON.parse(readFileSync(new URL('../../openclaw.plugin.json', import.meta.url), 'utf8'));
6
6
  test('manifest declares OpenClaw 2026 command ownership for admin bridge CLI', () => {
7
7
  assert.deepEqual(manifest.activation?.onCommands, ['admin-bridge', 'rol-websocket-channel']);
@@ -60,3 +60,26 @@ test('runtime CLI registrar exposes admin-bridge and rol-websocket-channel comma
60
60
  assert.equal(commands[0], 'admin-bridge');
61
61
  assert.equal(aliases[0], 'rol-websocket-channel');
62
62
  });
63
+ test('CLI error formatter preserves diagnostic data for pairing failures', () => {
64
+ const error = new Error('mqttUrl is missing from pairing payload');
65
+ error.data = {
66
+ code: 'PAIR_CHANNEL_CONFIG_INVALID',
67
+ debug: {
68
+ rootKeys: ['channel'],
69
+ channelConfigKeys: ['mqtt_topic'],
70
+ hasExistingMqttUrl: false
71
+ }
72
+ };
73
+ assert.deepEqual(formatCliErrorPayload(error), {
74
+ ok: false,
75
+ error: {
76
+ message: 'mqttUrl is missing from pairing payload',
77
+ code: 'PAIR_CHANNEL_CONFIG_INVALID',
78
+ debug: {
79
+ rootKeys: ['channel'],
80
+ channelConfigKeys: ['mqtt_topic'],
81
+ hasExistingMqttUrl: false
82
+ }
83
+ }
84
+ });
85
+ });
@@ -69,28 +69,55 @@ async function exchangePairKey(key, endpoint, authOverride, existingMqttUrl) {
69
69
  }
70
70
  function normalizePairingPayload(raw, endpoint, existingMqttUrl) {
71
71
  const root = unwrapPayload(raw);
72
- const pluginId = pickString(root.pluginId) ?? DEFAULT_PLUGIN_ID;
73
- const apiCoreBotValue = isRecord(root.apiCoreBot) ? root.apiCoreBot : {};
74
- const channelValue = isRecord(root.channel) ? root.channel : {};
72
+ const pluginId = pickString(root.pluginId) ?? pickString(root.plugin_id) ?? DEFAULT_PLUGIN_ID;
73
+ const apiCoreBotValue = pickRecord(root.apiCoreBot, root.api_core_bot);
74
+ const channelValue = pickRecord(root.channel);
75
+ const channelConfigValue = pickRecord(channelValue.config);
75
76
  const apiCoreBotAuthToken = pickString(apiCoreBotValue.authToken)
77
+ ?? pickString(apiCoreBotValue.auth_token)
76
78
  ?? pickString(root.authToken)
79
+ ?? pickString(root.auth_token)
77
80
  ?? pickString(root.token);
78
- const rawValue = pickString(root.value);
81
+ const rawValue = pickString(root.value)
82
+ ?? pickString(root.rawValue)
83
+ ?? pickString(root.raw_value);
79
84
  const mqttUrl = pickString(channelValue.mqttUrl)
85
+ ?? pickString(channelValue.mqtt_url)
86
+ ?? pickString(channelConfigValue.mqttUrl)
87
+ ?? pickString(channelConfigValue.mqtt_url)
80
88
  ?? pickString(root.mqttUrl)
89
+ ?? pickString(root.mqtt_url)
81
90
  ?? existingMqttUrl
82
91
  ?? null;
83
92
  if (!mqttUrl) {
84
- throwPairingError('PAIR_CHANNEL_CONFIG_INVALID', 'mqttUrl is missing from pairing payload');
93
+ throwPairingError('PAIR_CHANNEL_CONFIG_INVALID', 'mqttUrl is missing from pairing payload', {
94
+ endpoint,
95
+ rootKeys: Object.keys(root),
96
+ channelKeys: Object.keys(channelValue),
97
+ channelConfigKeys: Object.keys(channelConfigValue),
98
+ hasExistingMqttUrl: Boolean(existingMqttUrl)
99
+ });
85
100
  }
86
101
  const mqttTopic = pickString(channelValue.mqttTopic)
102
+ ?? pickString(channelValue.mqtt_topic)
103
+ ?? pickString(channelConfigValue.mqttTopic)
104
+ ?? pickString(channelConfigValue.mqtt_topic)
87
105
  ?? pickString(root.mqttTopic)
106
+ ?? pickString(root.mqtt_topic)
88
107
  ?? rawValue
89
108
  ?? 'announcement/tester';
90
109
  const groupPolicy = normalizeGroupPolicy(pickString(channelValue.groupPolicy)
110
+ ?? pickString(channelValue.group_policy)
91
111
  ?? pickString(channelValue.dmPolicy)
112
+ ?? pickString(channelValue.dm_policy)
113
+ ?? pickString(channelConfigValue.groupPolicy)
114
+ ?? pickString(channelConfigValue.group_policy)
115
+ ?? pickString(channelConfigValue.dmPolicy)
116
+ ?? pickString(channelConfigValue.dm_policy)
92
117
  ?? pickString(root.groupPolicy)
118
+ ?? pickString(root.group_policy)
93
119
  ?? pickString(root.dmPolicy)
120
+ ?? pickString(root.dm_policy)
94
121
  ?? 'pairing');
95
122
  return {
96
123
  pluginId,
@@ -98,7 +125,7 @@ function normalizePairingPayload(raw, endpoint, existingMqttUrl) {
98
125
  ...(rawValue ? { rawValue } : {})
99
126
  },
100
127
  apiCoreBot: {
101
- baseUrl: pickString(apiCoreBotValue.baseUrl) ?? inferBaseUrl(endpoint),
128
+ baseUrl: pickString(apiCoreBotValue.baseUrl) ?? pickString(apiCoreBotValue.base_url) ?? inferBaseUrl(endpoint),
102
129
  ...(apiCoreBotAuthToken ? { authToken: apiCoreBotAuthToken } : {})
103
130
  },
104
131
  channel: {
@@ -260,9 +287,20 @@ function pickString(value) {
260
287
  const trimmed = value.trim();
261
288
  return trimmed.length > 0 ? trimmed : null;
262
289
  }
290
+ function pickRecord(...values) {
291
+ for (const value of values) {
292
+ if (isRecord(value)) {
293
+ return value;
294
+ }
295
+ }
296
+ return {};
297
+ }
263
298
  function isRecord(value) {
264
299
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
265
300
  }
266
- function throwPairingError(code, message) {
267
- throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, message, { code });
301
+ function throwPairingError(code, message, debug) {
302
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, message, {
303
+ code,
304
+ ...(debug ? { debug } : {})
305
+ });
268
306
  }
@@ -0,0 +1,114 @@
1
+ import { afterEach, describe, test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs/promises';
4
+ import http from 'node:http';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { pairWithKey } from './pairing.js';
8
+ import { JsonRpcException } from '../jsonrpc.js';
9
+ const tempDirs = [];
10
+ const servers = [];
11
+ afterEach(async () => {
12
+ while (servers.length > 0) {
13
+ const server = servers.pop();
14
+ if (server) {
15
+ await new Promise((resolve, reject) => {
16
+ server.close((error) => error ? reject(error) : resolve());
17
+ });
18
+ }
19
+ }
20
+ while (tempDirs.length > 0) {
21
+ const dir = tempDirs.pop();
22
+ if (dir) {
23
+ await fs.rm(dir, { recursive: true, force: true });
24
+ }
25
+ }
26
+ });
27
+ describe('pairWithKey payload compatibility', () => {
28
+ test('accepts snake_case channel config from the pairing endpoint', async () => {
29
+ const context = await createMethodContext();
30
+ const endpoint = await createPairingEndpoint({
31
+ data: {
32
+ plugin_id: 'rol-websocket-channel',
33
+ api_core_bot: {
34
+ base_url: 'https://api.example.test',
35
+ auth_token: 'secret-token'
36
+ },
37
+ value: 'announcement/user/agent/#',
38
+ channel: {
39
+ config: {
40
+ mqtt_url: 'ws://mqtt.example.test:8083/mqtt',
41
+ mqtt_topic: 'announcement/user/agent/#',
42
+ group_policy: 'pairing'
43
+ }
44
+ }
45
+ }
46
+ });
47
+ const result = await pairWithKey({ key: 'abc123456', endpoint }, context);
48
+ const savedConfig = JSON.parse(await fs.readFile(path.join(context.openclawRoot, 'openclaw.json'), 'utf8'));
49
+ assert.equal(result.ok, true);
50
+ assert.equal(result.channel.mqttUrl, 'ws://mqtt.example.test:8083/mqtt');
51
+ assert.equal(result.channel.mqttTopic, 'announcement/user/agent/#');
52
+ assert.equal(result.channel.groupPolicy, 'pairing');
53
+ assert.deepEqual(savedConfig.plugins.allow, ['rol-websocket-channel']);
54
+ assert.equal(savedConfig.channels['rol-websocket-channel'].config.mqttUrl, 'ws://mqtt.example.test:8083/mqtt');
55
+ assert.equal(savedConfig.plugins.entries['rol-websocket-channel'].config.apiCoreBot.authToken, 'secret-token');
56
+ });
57
+ test('reports payload keys when mqttUrl is missing', async () => {
58
+ const context = await createMethodContext();
59
+ const endpoint = await createPairingEndpoint({
60
+ data: {
61
+ plugin_id: 'rol-websocket-channel',
62
+ channel: {
63
+ config: {
64
+ mqtt_topic: 'announcement/user/agent/#'
65
+ }
66
+ }
67
+ }
68
+ });
69
+ await assert.rejects(() => pairWithKey({ key: 'abc123456', endpoint }, context), (error) => {
70
+ assert.ok(error instanceof JsonRpcException);
71
+ assert.equal(error.message, 'mqttUrl is missing from pairing payload');
72
+ assert.deepEqual(error.data, {
73
+ code: 'PAIR_CHANNEL_CONFIG_INVALID',
74
+ debug: {
75
+ endpoint,
76
+ rootKeys: ['plugin_id', 'channel'],
77
+ channelKeys: ['config'],
78
+ channelConfigKeys: ['mqtt_topic'],
79
+ hasExistingMqttUrl: false
80
+ }
81
+ });
82
+ return true;
83
+ });
84
+ });
85
+ });
86
+ async function createMethodContext() {
87
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'rol-pairing-'));
88
+ tempDirs.push(dir);
89
+ await fs.writeFile(path.join(dir, 'openclaw.json'), '{}\n', 'utf8');
90
+ return {
91
+ projectRoot: dir,
92
+ openclawRoot: dir
93
+ };
94
+ }
95
+ async function createPairingEndpoint(payload) {
96
+ const server = http.createServer(async (request, response) => {
97
+ if (request.method !== 'POST') {
98
+ response.writeHead(405).end();
99
+ return;
100
+ }
101
+ for await (const _chunk of request) {
102
+ // Drain request body so Node can close the connection cleanly.
103
+ }
104
+ response.writeHead(200, { 'Content-Type': 'application/json' });
105
+ response.end(JSON.stringify(payload));
106
+ });
107
+ await new Promise((resolve) => {
108
+ server.listen(0, '127.0.0.1', resolve);
109
+ });
110
+ servers.push(server);
111
+ const address = server.address();
112
+ assert.ok(address && typeof address === 'object');
113
+ return `http://127.0.0.1:${address.port}/pair`;
114
+ }
package/index.ts CHANGED
@@ -38,6 +38,28 @@ export function getPluginRuntime(): any {
38
38
  return pluginRuntime;
39
39
  }
40
40
 
41
+ export function formatCliErrorPayload(error: unknown): {
42
+ ok: false;
43
+ error: Record<string, unknown>;
44
+ } {
45
+ const data = (error as { data?: unknown } | null | undefined)?.data;
46
+ const dataObject =
47
+ data && typeof data === "object" && !Array.isArray(data)
48
+ ? (data as Record<string, unknown>)
49
+ : {};
50
+ const code =
51
+ dataObject.code ?? (error as { code?: unknown } | null | undefined)?.code;
52
+
53
+ return {
54
+ ok: false,
55
+ error: {
56
+ message: error instanceof Error ? error.message : String(error),
57
+ ...dataObject,
58
+ ...(code === undefined ? {} : { code }),
59
+ },
60
+ };
61
+ }
62
+
41
63
  // ============================================
42
64
  // 4. 插件主体定义
43
65
  // ============================================
@@ -725,18 +747,7 @@ function registerAdminBridgeCli(api: any) {
725
747
  } catch (error) {
726
748
  process.exitCode = 1;
727
749
  process.stderr.write(
728
- JSON.stringify(
729
- {
730
- ok: false,
731
- error: {
732
- message:
733
- error instanceof Error ? error.message : String(error),
734
- code: (error as any)?.data?.code,
735
- },
736
- },
737
- null,
738
- 2,
739
- ) + "\n",
750
+ JSON.stringify(formatCliErrorPayload(error), null, 2) + "\n",
740
751
  );
741
752
  }
742
753
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rol-websocket-channel",
3
- "version": "1.4.9",
3
+ "version": "1.5.2",
4
4
  "description": "Unified OpenClaw plugin: MQTT Channel + Admin Bridge for remote management",
5
5
  "license": "MIT",
6
6
  "author": "nixgnehc",
@@ -1,7 +1,7 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import test from 'node:test';
4
- import register from '../../index.js';
4
+ import register, { formatCliErrorPayload } from '../../index.js';
5
5
 
6
6
  const manifest = JSON.parse(readFileSync(new URL('../../openclaw.plugin.json', import.meta.url), 'utf8'));
7
7
 
@@ -65,3 +65,30 @@ test('runtime CLI registrar exposes admin-bridge and rol-websocket-channel comma
65
65
  assert.equal(commands[0], 'admin-bridge');
66
66
  assert.equal(aliases[0], 'rol-websocket-channel');
67
67
  });
68
+
69
+ test('CLI error formatter preserves diagnostic data for pairing failures', () => {
70
+ const error = new Error('mqttUrl is missing from pairing payload') as Error & {
71
+ data: Record<string, unknown>;
72
+ };
73
+ error.data = {
74
+ code: 'PAIR_CHANNEL_CONFIG_INVALID',
75
+ debug: {
76
+ rootKeys: ['channel'],
77
+ channelConfigKeys: ['mqtt_topic'],
78
+ hasExistingMqttUrl: false
79
+ }
80
+ };
81
+
82
+ assert.deepEqual(formatCliErrorPayload(error), {
83
+ ok: false,
84
+ error: {
85
+ message: 'mqttUrl is missing from pairing payload',
86
+ code: 'PAIR_CHANNEL_CONFIG_INVALID',
87
+ debug: {
88
+ rootKeys: ['channel'],
89
+ channelConfigKeys: ['mqtt_topic'],
90
+ hasExistingMqttUrl: false
91
+ }
92
+ }
93
+ });
94
+ });
@@ -0,0 +1,149 @@
1
+ import { afterEach, describe, test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs/promises';
4
+ import http from 'node:http';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+
8
+ import { pairWithKey } from './pairing.js';
9
+ import { JsonRpcException } from '../jsonrpc.js';
10
+ import type { MethodContext } from '../types.js';
11
+
12
+ const tempDirs: string[] = [];
13
+ const servers: http.Server[] = [];
14
+
15
+ afterEach(async () => {
16
+ while (servers.length > 0) {
17
+ const server = servers.pop();
18
+ if (server) {
19
+ await new Promise<void>((resolve, reject) => {
20
+ server.close((error) => error ? reject(error) : resolve());
21
+ });
22
+ }
23
+ }
24
+
25
+ while (tempDirs.length > 0) {
26
+ const dir = tempDirs.pop();
27
+ if (dir) {
28
+ await fs.rm(dir, { recursive: true, force: true });
29
+ }
30
+ }
31
+ });
32
+
33
+ describe('pairWithKey payload compatibility', () => {
34
+ test('accepts snake_case channel config from the pairing endpoint', async () => {
35
+ const context = await createMethodContext();
36
+ const endpoint = await createPairingEndpoint({
37
+ data: {
38
+ plugin_id: 'rol-websocket-channel',
39
+ api_core_bot: {
40
+ base_url: 'https://api.example.test',
41
+ auth_token: 'secret-token'
42
+ },
43
+ value: 'announcement/user/agent/#',
44
+ channel: {
45
+ config: {
46
+ mqtt_url: 'ws://mqtt.example.test:8083/mqtt',
47
+ mqtt_topic: 'announcement/user/agent/#',
48
+ group_policy: 'pairing'
49
+ }
50
+ }
51
+ }
52
+ });
53
+
54
+ const result = await pairWithKey({ key: 'abc123456', endpoint }, context) as {
55
+ ok: boolean;
56
+ channel: {
57
+ mqttUrl: string;
58
+ mqttTopic: string;
59
+ groupPolicy: string;
60
+ };
61
+ };
62
+ const savedConfig = JSON.parse(
63
+ await fs.readFile(path.join(context.openclawRoot, 'openclaw.json'), 'utf8')
64
+ );
65
+
66
+ assert.equal(result.ok, true);
67
+ assert.equal(result.channel.mqttUrl, 'ws://mqtt.example.test:8083/mqtt');
68
+ assert.equal(result.channel.mqttTopic, 'announcement/user/agent/#');
69
+ assert.equal(result.channel.groupPolicy, 'pairing');
70
+ assert.deepEqual(savedConfig.plugins.allow, ['rol-websocket-channel']);
71
+ assert.equal(
72
+ savedConfig.channels['rol-websocket-channel'].config.mqttUrl,
73
+ 'ws://mqtt.example.test:8083/mqtt'
74
+ );
75
+ assert.equal(
76
+ savedConfig.plugins.entries['rol-websocket-channel'].config.apiCoreBot.authToken,
77
+ 'secret-token'
78
+ );
79
+ });
80
+
81
+ test('reports payload keys when mqttUrl is missing', async () => {
82
+ const context = await createMethodContext();
83
+ const endpoint = await createPairingEndpoint({
84
+ data: {
85
+ plugin_id: 'rol-websocket-channel',
86
+ channel: {
87
+ config: {
88
+ mqtt_topic: 'announcement/user/agent/#'
89
+ }
90
+ }
91
+ }
92
+ });
93
+
94
+ await assert.rejects(
95
+ () => pairWithKey({ key: 'abc123456', endpoint }, context),
96
+ (error: unknown) => {
97
+ assert.ok(error instanceof JsonRpcException);
98
+ assert.equal(error.message, 'mqttUrl is missing from pairing payload');
99
+ assert.deepEqual(error.data, {
100
+ code: 'PAIR_CHANNEL_CONFIG_INVALID',
101
+ debug: {
102
+ endpoint,
103
+ rootKeys: ['plugin_id', 'channel'],
104
+ channelKeys: ['config'],
105
+ channelConfigKeys: ['mqtt_topic'],
106
+ hasExistingMqttUrl: false
107
+ }
108
+ });
109
+ return true;
110
+ }
111
+ );
112
+ });
113
+ });
114
+
115
+ async function createMethodContext(): Promise<MethodContext> {
116
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'rol-pairing-'));
117
+ tempDirs.push(dir);
118
+ await fs.writeFile(path.join(dir, 'openclaw.json'), '{}\n', 'utf8');
119
+ return {
120
+ projectRoot: dir,
121
+ openclawRoot: dir
122
+ };
123
+ }
124
+
125
+ async function createPairingEndpoint(payload: unknown): Promise<string> {
126
+ const server = http.createServer(async (request, response) => {
127
+ if (request.method !== 'POST') {
128
+ response.writeHead(405).end();
129
+ return;
130
+ }
131
+
132
+ for await (const _chunk of request) {
133
+ // Drain request body so Node can close the connection cleanly.
134
+ }
135
+
136
+ response.writeHead(200, { 'Content-Type': 'application/json' });
137
+ response.end(JSON.stringify(payload));
138
+ });
139
+
140
+ await new Promise<void>((resolve) => {
141
+ server.listen(0, '127.0.0.1', resolve);
142
+ });
143
+ servers.push(server);
144
+
145
+ const address = server.address();
146
+ assert.ok(address && typeof address === 'object');
147
+
148
+ return `http://127.0.0.1:${address.port}/pair`;
149
+ }
@@ -46,6 +46,14 @@ interface PairingPayload {
46
46
  };
47
47
  }
48
48
 
49
+ interface PairingPayloadDebug {
50
+ endpoint: string;
51
+ rootKeys: string[];
52
+ channelKeys: string[];
53
+ channelConfigKeys: string[];
54
+ hasExistingMqttUrl: boolean;
55
+ }
56
+
49
57
  export async function pairWithKey(
50
58
  options: PairingCommandOptions,
51
59
  context: MethodContext
@@ -134,33 +142,60 @@ function normalizePairingPayload(
134
142
  existingMqttUrl?: string | null
135
143
  ): PairingPayload {
136
144
  const root = unwrapPayload(raw);
137
- const pluginId = pickString(root.pluginId) ?? DEFAULT_PLUGIN_ID;
138
- const apiCoreBotValue = isRecord(root.apiCoreBot) ? root.apiCoreBot : {};
139
- const channelValue = isRecord(root.channel) ? root.channel : {};
145
+ const pluginId = pickString(root.pluginId) ?? pickString(root.plugin_id) ?? DEFAULT_PLUGIN_ID;
146
+ const apiCoreBotValue = pickRecord(root.apiCoreBot, root.api_core_bot);
147
+ const channelValue = pickRecord(root.channel);
148
+ const channelConfigValue = pickRecord(channelValue.config);
140
149
 
141
150
  const apiCoreBotAuthToken = pickString(apiCoreBotValue.authToken)
151
+ ?? pickString(apiCoreBotValue.auth_token)
142
152
  ?? pickString(root.authToken)
153
+ ?? pickString(root.auth_token)
143
154
  ?? pickString(root.token);
144
- const rawValue = pickString(root.value);
155
+ const rawValue = pickString(root.value)
156
+ ?? pickString(root.rawValue)
157
+ ?? pickString(root.raw_value);
145
158
 
146
159
  const mqttUrl = pickString(channelValue.mqttUrl)
160
+ ?? pickString(channelValue.mqtt_url)
161
+ ?? pickString(channelConfigValue.mqttUrl)
162
+ ?? pickString(channelConfigValue.mqtt_url)
147
163
  ?? pickString(root.mqttUrl)
164
+ ?? pickString(root.mqtt_url)
148
165
  ?? existingMqttUrl
149
166
  ?? null;
150
167
  if (!mqttUrl) {
151
- throwPairingError('PAIR_CHANNEL_CONFIG_INVALID', 'mqttUrl is missing from pairing payload');
168
+ throwPairingError('PAIR_CHANNEL_CONFIG_INVALID', 'mqttUrl is missing from pairing payload', {
169
+ endpoint,
170
+ rootKeys: Object.keys(root),
171
+ channelKeys: Object.keys(channelValue),
172
+ channelConfigKeys: Object.keys(channelConfigValue),
173
+ hasExistingMqttUrl: Boolean(existingMqttUrl)
174
+ });
152
175
  }
153
176
 
154
177
  const mqttTopic = pickString(channelValue.mqttTopic)
178
+ ?? pickString(channelValue.mqtt_topic)
179
+ ?? pickString(channelConfigValue.mqttTopic)
180
+ ?? pickString(channelConfigValue.mqtt_topic)
155
181
  ?? pickString(root.mqttTopic)
182
+ ?? pickString(root.mqtt_topic)
156
183
  ?? rawValue
157
184
  ?? 'announcement/tester';
158
185
 
159
186
  const groupPolicy = normalizeGroupPolicy(
160
187
  pickString(channelValue.groupPolicy)
188
+ ?? pickString(channelValue.group_policy)
161
189
  ?? pickString(channelValue.dmPolicy)
190
+ ?? pickString(channelValue.dm_policy)
191
+ ?? pickString(channelConfigValue.groupPolicy)
192
+ ?? pickString(channelConfigValue.group_policy)
193
+ ?? pickString(channelConfigValue.dmPolicy)
194
+ ?? pickString(channelConfigValue.dm_policy)
162
195
  ?? pickString(root.groupPolicy)
196
+ ?? pickString(root.group_policy)
163
197
  ?? pickString(root.dmPolicy)
198
+ ?? pickString(root.dm_policy)
164
199
  ?? 'pairing'
165
200
  );
166
201
 
@@ -170,7 +205,7 @@ function normalizePairingPayload(
170
205
  ...(rawValue ? { rawValue } : {})
171
206
  },
172
207
  apiCoreBot: {
173
- baseUrl: pickString(apiCoreBotValue.baseUrl) ?? inferBaseUrl(endpoint),
208
+ baseUrl: pickString(apiCoreBotValue.baseUrl) ?? pickString(apiCoreBotValue.base_url) ?? inferBaseUrl(endpoint),
174
209
  ...(apiCoreBotAuthToken ? { authToken: apiCoreBotAuthToken } : {})
175
210
  },
176
211
  channel: {
@@ -352,10 +387,23 @@ function pickString(value: unknown): string | null {
352
387
  return trimmed.length > 0 ? trimmed : null;
353
388
  }
354
389
 
390
+ function pickRecord(...values: unknown[]): Record<string, any> {
391
+ for (const value of values) {
392
+ if (isRecord(value)) {
393
+ return value;
394
+ }
395
+ }
396
+
397
+ return {};
398
+ }
399
+
355
400
  function isRecord(value: unknown): value is Record<string, any> {
356
401
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
357
402
  }
358
403
 
359
- function throwPairingError(code: string, message: string): never {
360
- throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, message, { code });
404
+ function throwPairingError(code: string, message: string, debug?: PairingPayloadDebug): never {
405
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, message, {
406
+ code,
407
+ ...(debug ? { debug } : {})
408
+ });
361
409
  }