@xmanrui/dsh-im 4.17.0 → 4.17.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "4.17.0",
3
+ "version": "4.17.1",
4
4
  "description": "把十种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect ten IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -97,12 +97,13 @@
97
97
  "platform": "web"
98
98
  },
99
99
  "compatibility": {
100
- "dsh": "0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.3-alpha.1",
100
+ "dsh": "0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.3-alpha.1 || 0.1.5-alpha.1",
101
101
  "dshReleases": {
102
102
  "0.1.2-alpha.4": "compatible",
103
103
  "0.1.2-alpha.5": "compatible",
104
104
  "0.1.2-rc.1": "compatible",
105
- "0.1.3-alpha.1": "compatible"
105
+ "0.1.3-alpha.1": "compatible",
106
+ "0.1.5-alpha.1": "compatible"
106
107
  },
107
108
  "profiles": [
108
109
  "web"
@@ -1,3 +1,4 @@
1
+ import { callManagementRpc } from '../management-rpc.mjs';
1
2
  import * as React from 'react';
2
3
  import manifest from '../../package.json' with { type: 'json' };
3
4
 
@@ -403,33 +404,33 @@ export function apply(ctx) {
403
404
  }, 'im-settings: install combined channel styles');
404
405
 
405
406
  const feishuRpcCall = (endpoint, payload, signal) =>
406
- ctx.connection.rpc.call(FEISHU_RPC_CHANNEL, endpoint, payload, signal);
407
+ callManagementRpc(ctx.connection, FEISHU_RPC_CHANNEL, endpoint, payload, signal);
407
408
  const weixinRpcCall = (endpoint, payload, signal) =>
408
- ctx.connection.rpc.call(WEIXIN_RPC_CHANNEL, endpoint, payload, signal);
409
+ callManagementRpc(ctx.connection, WEIXIN_RPC_CHANNEL, endpoint, payload, signal);
409
410
  const dingtalkRpcCall = (endpoint, payload, signal) =>
410
- ctx.connection.rpc.call(DINGTALK_RPC_CHANNEL, endpoint, payload, signal);
411
+ callManagementRpc(ctx.connection, DINGTALK_RPC_CHANNEL, endpoint, payload, signal);
411
412
  const qqRpcCall = (endpoint, payload, signal) =>
412
- ctx.connection.rpc.call(QQ_RPC_CHANNEL, endpoint, payload, signal);
413
+ callManagementRpc(ctx.connection, QQ_RPC_CHANNEL, endpoint, payload, signal);
413
414
  const wecomRpcCall = (endpoint, payload, signal) =>
414
- ctx.connection.rpc.call(WECOM_RPC_CHANNEL, endpoint, payload, signal);
415
+ callManagementRpc(ctx.connection, WECOM_RPC_CHANNEL, endpoint, payload, signal);
415
416
  const wecomAppRpcCall = (endpoint, payload, signal) =>
416
- ctx.connection.rpc.call(WECOM_APP_RPC_CHANNEL, endpoint, payload, signal);
417
+ callManagementRpc(ctx.connection, WECOM_APP_RPC_CHANNEL, endpoint, payload, signal);
417
418
  const telegramRpcCall = (endpoint, payload, signal) =>
418
- ctx.connection.rpc.call(TELEGRAM_RPC_CHANNEL, endpoint, payload, signal);
419
+ callManagementRpc(ctx.connection, TELEGRAM_RPC_CHANNEL, endpoint, payload, signal);
419
420
  const discordRpcCall = (endpoint, payload, signal) =>
420
- ctx.connection.rpc.call(DISCORD_RPC_CHANNEL, endpoint, payload, signal);
421
+ callManagementRpc(ctx.connection, DISCORD_RPC_CHANNEL, endpoint, payload, signal);
421
422
  const whatsappRpcCall = (endpoint, payload, signal) =>
422
- ctx.connection.rpc.call(WHATSAPP_RPC_CHANNEL, endpoint, payload, signal);
423
+ callManagementRpc(ctx.connection, WHATSAPP_RPC_CHANNEL, endpoint, payload, signal);
423
424
  const slackRpcCall = (endpoint, payload, signal) =>
424
- ctx.connection.rpc.call(SLACK_RPC_CHANNEL, endpoint, payload, signal);
425
+ callManagementRpc(ctx.connection, SLACK_RPC_CHANNEL, endpoint, payload, signal);
425
426
  const officeRpcCall = (endpoint, payload, signal) =>
426
- ctx.connection.rpc.call(OFFICE_RPC_CHANNEL, endpoint, payload, signal);
427
+ callManagementRpc(ctx.connection, OFFICE_RPC_CHANNEL, endpoint, payload, signal);
427
428
  const updateRpcCall = (endpoint, payload, signal) =>
428
- ctx.connection.rpc.call(UPDATE_RPC_CHANNEL, endpoint, payload, signal);
429
+ callManagementRpc(ctx.connection, UPDATE_RPC_CHANNEL, endpoint, payload, signal);
429
430
  const deliveryRpcCall = (endpoint, payload, signal) =>
430
- ctx.connection.rpc.call(DELIVERY_RPC_CHANNEL, endpoint, payload, signal);
431
+ callManagementRpc(ctx.connection, DELIVERY_RPC_CHANNEL, endpoint, payload, signal);
431
432
  const globalSettingsRpcCall = (endpoint, payload, signal) =>
432
- ctx.connection.rpc.call(GLOBAL_SETTINGS_RPC_CHANNEL, endpoint, payload, signal);
433
+ callManagementRpc(ctx.connection, GLOBAL_SETTINGS_RPC_CHANNEL, endpoint, payload, signal);
433
434
  const workspaceDirectoryPicker = Object.freeze({
434
435
  listDirectory: (path, signal) =>
435
436
  callWorkspaceDirectoryApi(ctx, 'listDirectory', path, signal),
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
1
2
  import QRCode from 'qrcode';
2
3
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
3
4
  import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
@@ -336,10 +337,7 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
336
337
  }
337
338
 
338
339
  export function installDingtalkRpc(ctx, controller, options, authority) {
339
- if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
340
- throw new TypeError('DSH Host Connection RPC is required');
341
- }
342
- return ctx.connection.rpc.handle(
340
+ return registerManagementRpc(ctx,
343
341
  DINGTALK_RPC_CHANNEL,
344
342
  createDingtalkRpcHandler(controller, options),
345
343
  { authority: resolveRpcAuthority(authority) },
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
1
2
  import QRCode from 'qrcode';
2
3
  import {
3
4
  normalizeAgentPresetCatalog,
@@ -795,10 +796,7 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
795
796
 
796
797
  /** Register the `/feishu` logical channel with its configured browser authority. */
797
798
  export function installFeishuRpc(ctx, controller, options, authority) {
798
- if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
799
- throw new TypeError('DSH Host Connection RPC is required');
800
- }
801
- return ctx.connection.rpc.handle(
799
+ return registerManagementRpc(ctx,
802
800
  FEISHU_RPC_CHANNEL,
803
801
  createFeishuRpcHandler(controller, options),
804
802
  { authority: resolveRpcAuthority(authority) },
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
1
2
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
2
3
  import { OFFICE_RPC_CHANNEL, OFFICE_RPC_ENDPOINTS } from '../../../../src/channels/office/protocol.mjs';
3
4
 
@@ -40,7 +41,7 @@ export function createOfficeRpcHandler(controller) {
40
41
  }
41
42
 
42
43
  export function installOfficeRpc(ctx, controller, authority) {
43
- return ctx.connection.rpc.handle(
44
+ return registerManagementRpc(ctx,
44
45
  OFFICE_RPC_CHANNEL,
45
46
  createOfficeRpcHandler(controller),
46
47
  { authority: resolveRpcAuthority(authority) },
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
1
2
  import QRCode from 'qrcode';
2
3
  import {
3
4
  connectionTestTargetUnavailable,
@@ -227,10 +228,7 @@ export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
227
228
  }
228
229
 
229
230
  export function installQqRpc(ctx, controller, options, authority) {
230
- if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
231
- throw new TypeError('DSH Host Connection RPC is required');
232
- }
233
- return ctx.connection.rpc.handle(
231
+ return registerManagementRpc(ctx,
234
232
  QQ_RPC_CHANNEL,
235
233
  createQqRpcHandler(controller, options),
236
234
  { authority: resolveRpcAuthority(authority) },
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
1
2
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from './context-enhancement-rpc.mjs';
2
3
  import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from './access-policy-rpc.mjs';
3
4
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
@@ -197,10 +198,7 @@ export function createTokenBotRpcHandler(controller, { channel }) {
197
198
  }
198
199
 
199
200
  export function installTokenBotRpc(ctx, controller, { channel, rpcChannel, authority }) {
200
- if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
201
- throw new TypeError('DSH Host Connection RPC is required');
202
- }
203
- return ctx.connection.rpc.handle(
201
+ return registerManagementRpc(ctx,
204
202
  rpcChannel,
205
203
  createTokenBotRpcHandler(controller, { channel }),
206
204
  { authority: resolveRpcAuthority(authority) },
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
1
2
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
2
3
  import { publicChannelInitializing, publicChannelStartupError } from './startup-error.mjs';
3
4
 
@@ -5,12 +6,9 @@ import { publicChannelInitializing, publicChannelStartupError } from './startup-
5
6
  export async function installProductionChannel(ctx, config, {
6
7
  channel, rpcChannel, createProduction, createHandler,
7
8
  }) {
8
- if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
9
- throw new TypeError('DSH Host Connection RPC is required');
10
- }
11
9
  let startupError = publicChannelInitializing(channel);
12
10
  let handler = async () => ({ ok: false, error: startupError });
13
- const disposeRpc = ctx.connection.rpc.handle(rpcChannel, (endpoint, payload, signal) => {
11
+ const disposeRpc = registerManagementRpc(ctx, rpcChannel, (endpoint, payload, signal) => {
14
12
  if (signal?.aborted) {
15
13
  return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.', details: {} } };
16
14
  }
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
1
2
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
2
3
  import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
3
4
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
@@ -200,10 +201,7 @@ export function createSlackRpcHandler(controller) {
200
201
  }
201
202
 
202
203
  export function installSlackRpc(ctx, controller, authority) {
203
- if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
204
- throw new TypeError('DSH Host Connection RPC is required');
205
- }
206
- return ctx.connection.rpc.handle(
204
+ return registerManagementRpc(ctx,
207
205
  SLACK_RPC_CHANNEL,
208
206
  createSlackRpcHandler(controller),
209
207
  { authority: resolveRpcAuthority(authority) },
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
1
2
  import {
2
3
  TOKEN_BOT_ENDPOINTS,
3
4
  createTokenBotRpcHandler,
@@ -13,10 +14,7 @@ export function createTelegramRpcHandler(controller) {
13
14
  }
14
15
 
15
16
  export function installTelegramRpc(ctx, controller, authority) {
16
- if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
17
- throw new TypeError('DSH Host Connection RPC is required');
18
- }
19
- return ctx.connection.rpc.handle(
17
+ return registerManagementRpc(ctx,
20
18
  TELEGRAM_RPC_CHANNEL,
21
19
  createTelegramRpcHandler(controller),
22
20
  { authority: resolveRpcAuthority(authority) },
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
1
2
  import QRCode from 'qrcode';
2
3
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
3
4
  import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
@@ -233,10 +234,7 @@ export function installWecomRpc(ctx, controller, options, authority) {
233
234
 
234
235
  /** Mount the existing Connection transport before the production controller is ready. */
235
236
  export function installWecomRpcHandler(ctx, handler, authority) {
236
- if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
237
- throw new TypeError('DSH Host Connection RPC is required');
238
- }
239
- return ctx.connection.rpc.handle(
237
+ return registerManagementRpc(ctx,
240
238
  WECOM_RPC_CHANNEL,
241
239
  handler,
242
240
  { authority: resolveRpcAuthority(authority) },
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
1
2
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
2
3
  import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
3
4
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
@@ -214,10 +215,7 @@ export function createWecomAppRpcHandler(controller) {
214
215
  }
215
216
 
216
217
  export function installWecomAppRpc(ctx, controller, options, authority) {
217
- if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
218
- throw new TypeError('DSH Host Connection RPC is required');
219
- }
220
- return ctx.connection.rpc.handle(
218
+ return registerManagementRpc(ctx,
221
219
  WECOM_APP_RPC_CHANNEL,
222
220
  createWecomAppRpcHandler(controller, options),
223
221
  { authority: resolveRpcAuthority(authority) },
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
1
2
  import QRCode from 'qrcode';
2
3
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
3
4
  import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
@@ -260,10 +261,7 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}
260
261
  }
261
262
 
262
263
  export function installWeixinRpc(ctx, controller, options, authority) {
263
- if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
264
- throw new TypeError('DSH Host Connection RPC is required');
265
- }
266
- return ctx.connection.rpc.handle(
264
+ return registerManagementRpc(ctx,
267
265
  WEIXIN_RPC_CHANNEL,
268
266
  createWeixinRpcHandler(controller, options),
269
267
  { authority: resolveRpcAuthority(authority) },
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
1
2
  import QRCode from 'qrcode';
2
3
 
3
4
  import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
@@ -218,10 +219,7 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
218
219
  }
219
220
 
220
221
  export function installWhatsappRpc(ctx, controller, options, authority) {
221
- if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
222
- throw new TypeError('DSH Host Connection RPC is required');
223
- }
224
- return ctx.connection.rpc.handle(
222
+ return registerManagementRpc(ctx,
225
223
  WHATSAPP_RPC_CHANNEL,
226
224
  createWhatsappRpcHandler(controller, options),
227
225
  { authority: resolveRpcAuthority(authority) },
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../management-rpc.mjs';
1
2
  import { resolveRpcAuthority } from './rpc-authority.mjs';
2
3
 
3
4
  export const DELIVERY_RPC_CHANNEL = '/dsh-im-delivery';
@@ -159,10 +160,7 @@ export function createDeliveryRpcHandler(service) {
159
160
  }
160
161
 
161
162
  export function installDeliveryRpc(ctx, service, { authority } = {}) {
162
- if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
163
- throw new TypeError('DSH Host Connection RPC is required');
164
- }
165
- return ctx.connection.rpc.handle(
163
+ return registerManagementRpc(ctx,
166
164
  DELIVERY_RPC_CHANNEL,
167
165
  createDeliveryRpcHandler(service),
168
166
  { authority: resolveRpcAuthority(authority) },
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../management-rpc.mjs';
1
2
  import { normalizeInboundTtlHours } from '../../src/channels/shared/inbound-ttl.mjs';
2
3
  import { getInboundTtlRuntime } from './inbound-ttl-runtime.mjs';
3
4
 
@@ -69,13 +70,10 @@ export function createInboundTtlRpcHandler({ store, service, logger = null } = {
69
70
  }
70
71
 
71
72
  export function installInboundTtlRpc(ctx, options = {}) {
72
- if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
73
- throw new TypeError('DSH Host Connection RPC is required');
74
- }
75
73
  const runtime = options.runtime ?? getInboundTtlRuntime(ctx, options.config);
76
74
  const logger = typeof ctx?.logger === 'function'
77
75
  ? ctx.logger('dsh-im:inbound-ttl') : (ctx?.logger ?? null);
78
- return ctx.connection.rpc.handle(
76
+ return registerManagementRpc(ctx,
79
77
  INBOUND_TTL_RPC_CHANNEL,
80
78
  createInboundTtlRpcHandler({ ...runtime, logger }),
81
79
  { authority: 'loopback' },
@@ -130,7 +130,7 @@ export function createImHostPlugin(internals = {}) {
130
130
  const logger = typeof ctx?.logger === 'function'
131
131
  ? ctx.logger(name)
132
132
  : (ctx?.logger ?? console);
133
- if (ctx?.connection?.rpc) {
133
+ if (ctx?.connection?.fetch) {
134
134
  try {
135
135
  startUpdate(ctx);
136
136
  } catch (error) {
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../management-rpc.mjs';
1
2
  import { createUpdateRuntime } from './update-runtime.mjs';
2
3
  import { createUpdateService } from './update-service.mjs';
3
4
 
@@ -42,7 +43,7 @@ export function createUpdateRpcHandler(service) {
42
43
  export function installUpdateRpc(ctx, options = {}) {
43
44
  const runtime = options.runtime ?? createUpdateRuntime({ ctx, moduleUrl: import.meta.url });
44
45
  const service = options.service ?? createUpdateService({ runtime });
45
- const dispose = ctx.connection.rpc.handle(UPDATE_RPC_CHANNEL, createUpdateRpcHandler(service), {
46
+ const dispose = registerManagementRpc(ctx, UPDATE_RPC_CHANNEL, createUpdateRpcHandler(service), {
46
47
  authority: 'loopback',
47
48
  });
48
49
  ctx.effect(() => () => service.close(), 'dsh-im: close update installer');
@@ -0,0 +1,77 @@
1
+ import { resolveRpcAuthority } from './host/rpc-authority.mjs';
2
+
3
+ function rpcEndpoint(channel) {
4
+ if (!/^\/[A-Za-z0-9._~-]+$/.test(channel)) throw new TypeError('Invalid IM RPC channel');
5
+ return `dsh-im${channel}`;
6
+ }
7
+
8
+ function isRecord(value) {
9
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
10
+ }
11
+
12
+ function isLoopbackAuthority(authority) {
13
+ if (!authority) return false;
14
+ try {
15
+ const url = new URL(`http://${authority}`);
16
+ if (url.username || url.password || url.pathname !== '/' || url.search || url.hash) return false;
17
+ const hostname = url.hostname.replace(/\.$/, '');
18
+ return hostname === 'localhost' || hostname === '[::1]'
19
+ || /^127\.\d+\.\d+\.\d+$/.test(hostname);
20
+ } catch { return false; }
21
+ }
22
+
23
+ function isLoopbackRequest(request) {
24
+ if (!isLoopbackAuthority(request.headers.get('host'))) return false;
25
+ const origin = request.headers.get('origin');
26
+ if (origin === null) return true;
27
+ try {
28
+ const url = new URL(origin);
29
+ return ['http:', 'https:'].includes(url.protocol) && isLoopbackAuthority(url.host);
30
+ } catch { return false; }
31
+ }
32
+
33
+ function reply(rpcId, result) {
34
+ // Older endpoint handlers omit details; Connection's client requires the field.
35
+ const value = result.ok === false
36
+ ? { ...result, error: { ...result.error, details: result.error.details ?? {} } } : result;
37
+ return Response.json({ type: 'server-response', rpcId, result: value });
38
+ }
39
+
40
+ /** Register an IM channel through the public /api carrier shared by supported DSH releases. */
41
+ export function registerManagementRpc(ctx, channel, handler, { authority } = {}) {
42
+ const policy = resolveRpcAuthority(authority);
43
+ const endpoint = rpcEndpoint(channel);
44
+ if (typeof ctx?.connection?.fetch?.register !== 'function') {
45
+ throw new TypeError('DSH Host Connection Fetch registry is required');
46
+ }
47
+ return ctx.connection.fetch.register({
48
+ path: `/api/${endpoint}`,
49
+ methods: ['POST'],
50
+ requestBody: 'buffered',
51
+ async fetch(request) {
52
+ // DSH applies browser authentication and Host/Origin trust before this handler.
53
+ if (policy === 'loopback' && !isLoopbackRequest(request)) return new Response('forbidden', { status: 403 });
54
+ if (request.method !== 'POST') return new Response('method not allowed', { status: 405 });
55
+ if (request.headers.get('content-type')?.split(';', 1)[0].trim().toLowerCase() !== 'application/json') {
56
+ return new Response('content type must be application/json', { status: 415 });
57
+ }
58
+ let message;
59
+ try { message = await request.json(); }
60
+ catch { return new Response('body is not JSON', { status: 400 }); }
61
+ const rpcId = typeof message?.rpcId === 'string' ? message.rpcId : 'invalid-request';
62
+ const call = message?.payload;
63
+ if (!isRecord(message) || message.type !== 'client-request' || typeof message.rpcId !== 'string'
64
+ || message.method !== endpoint || !isRecord(call) || typeof call.method !== 'string'
65
+ || !Object.hasOwn(call, 'payload')) {
66
+ return reply(rpcId, { ok: false, error: { code: 'gateway/bad-request', message: 'Invalid IM management request.' } });
67
+ }
68
+ try { return reply(rpcId, await handler(call.method, call.payload, request.signal)); }
69
+ catch { return new Response('IM management handler failed', { status: 500 }); }
70
+ },
71
+ });
72
+ }
73
+
74
+ /** Keep channel UI callers unchanged while using DSH's native correlation and response validation. */
75
+ export function callManagementRpc(connection, channel, method, payload, signal) {
76
+ return connection.rpc.call('/api', rpcEndpoint(channel), { method, payload }, signal);
77
+ }