@zhin.js/host-http 1.0.0 → 1.0.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.
@@ -1,42 +1,5 @@
1
+ import { assertDemoConsoleRpcAllowed, endpointSendResult, normalizeConsoleRpcMessage, } from '@zhin.js/console-protocol';
1
2
  import { dispatchExtendedConsoleRpc } from './console-rpc-extended.js';
2
- /**
3
- * Frontend SDK (@zhin.js/client) calls colon-named aliases; normalize to the
4
- * canonical dot names before scope checks and dispatch. Alias parameter shapes
5
- * (`data.{adapter, endpointId, id, type, content, parent}`) are mapped onto the
6
- * `$`-prefixed fields inside each endpoint case.
7
- */
8
- const RPC_TYPE_ALIASES = Object.freeze({
9
- 'endpoint:list': 'endpoint.list',
10
- 'endpoint:info': 'endpoint.info',
11
- 'endpoint:sendMessage': 'endpoint.send_message',
12
- });
13
- /** ADR 0016 demo allowlist subset available on Plugin Runtime Host. */
14
- const DEMO_RPC_ALLOWLIST = new Set([
15
- 'ping',
16
- 'entries:get',
17
- 'pages:list',
18
- 'config:get',
19
- 'config:get-all',
20
- 'config:get-yaml',
21
- 'schema:get',
22
- 'schema:get-all',
23
- 'endpoint.list',
24
- 'endpoint.info',
25
- 'endpoint.send_message',
26
- ]);
27
- const DEMO_RPC_WRITE_BLOCKLIST = new Set([
28
- 'config:set',
29
- 'config:save-yaml',
30
- 'files:save',
31
- 'env:save',
32
- 'db:insert',
33
- 'db:update',
34
- 'db:delete',
35
- 'db:drop-table',
36
- 'db:kv:set',
37
- 'db:kv:delete',
38
- 'system:restart',
39
- ]);
40
3
  export function pickRpcReply(message, payloads) {
41
4
  const requestId = message.requestId;
42
5
  return ((requestId != null ? payloads.find((payload) => payload.requestId === requestId) : undefined)
@@ -48,11 +11,19 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
48
11
  const emit = (payload) => {
49
12
  payloads.push(payload);
50
13
  };
51
- const type = RPC_TYPE_ALIASES[String(message.type ?? '')] ?? String(message.type ?? '');
14
+ message = normalizeConsoleRpcMessage(message);
15
+ const type = String(message.type ?? '');
52
16
  const requestId = message.requestId;
53
- // Extended RPC surface (cron/schedule、endpoint 社交/inbox) — 自带 scope 矩阵
17
+ if (ctx.authScope === 'demo') {
18
+ const denied = assertDemoConsoleRpcAllowed(type);
19
+ if (denied) {
20
+ emit({ requestId, error: denied });
21
+ return payloads;
22
+ }
23
+ }
24
+ // Extended RPC surface (cron/schedule、endpoint 社交/inbox)
54
25
  if (ctx.extended) {
55
- const extended = await dispatchExtendedConsoleRpc(type, message.data, { ...ctx.extended, fullScope: ctx.authScope === 'full' });
26
+ const extended = await dispatchExtendedConsoleRpc(type, message, { ...ctx.extended, fullScope: ctx.authScope === 'full' });
56
27
  if (extended !== undefined) {
57
28
  emit('error' in extended
58
29
  ? { requestId, error: extended.error }
@@ -60,10 +31,6 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
60
31
  return payloads;
61
32
  }
62
33
  }
63
- if (ctx.authScope === 'demo' && (DEMO_RPC_WRITE_BLOCKLIST.has(type) || !DEMO_RPC_ALLOWLIST.has(type))) {
64
- emit({ requestId, error: `Demo scope: RPC "${type}" is forbidden` });
65
- return payloads;
66
- }
67
34
  switch (type) {
68
35
  case 'ping':
69
36
  emit({ type: 'pong', requestId });
@@ -118,7 +85,9 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
118
85
  }
119
86
  case 'config:get': {
120
87
  try {
121
- const key = String(message.key ?? message.pluginName ?? '');
88
+ // 兼容 console UI 的 data:{plugin} / data:{key} 形状
89
+ const data = (message.data ?? {});
90
+ const key = String(message.key ?? message.pluginName ?? data.pluginName ?? data.plugin ?? data.key ?? '');
122
91
  const document = ctx.readConfigDocument ? await ctx.readConfigDocument() : {};
123
92
  emit({ requestId, data: key ? document[key] : document });
124
93
  }
@@ -142,6 +111,7 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
142
111
  return payloads;
143
112
  }
144
113
  await ctx.writeConfigYaml(yaml);
114
+ ctx.publishEvent?.('config:updated', { pluginName: null, keys: [] });
145
115
  emit({
146
116
  requestId,
147
117
  data: { success: true, message: '配置已保存,需重启生效' },
@@ -167,6 +137,12 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
167
137
  return payloads;
168
138
  }
169
139
  const result = await ctx.setConfigKey(pluginName, message.data);
140
+ ctx.publishEvent?.('config:updated', {
141
+ pluginName,
142
+ keys: message.data && typeof message.data === 'object' && !Array.isArray(message.data)
143
+ ? Object.keys(message.data)
144
+ : [],
145
+ });
170
146
  emit({
171
147
  requestId,
172
148
  data: {
@@ -312,7 +288,10 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
312
288
  }
313
289
  case 'schema:get': {
314
290
  try {
315
- const pluginName = typeof message.pluginName === 'string' ? message.pluginName : undefined;
291
+ // 兼容 console UI 的 data:{plugin} / data:{pluginName} 形状(SDK 发顶层 pluginName)
292
+ const data = (message.data ?? {});
293
+ const candidate = message.pluginName ?? data.pluginName ?? data.plugin;
294
+ const pluginName = typeof candidate === 'string' ? candidate : undefined;
316
295
  const schema = ctx.getSchema ? await ctx.getSchema(pluginName) : null;
317
296
  emit({ requestId, data: schema });
318
297
  }
@@ -544,10 +523,9 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
544
523
  }
545
524
  case 'endpoint.info': {
546
525
  try {
547
- const data = (message.data ?? {});
548
- // Accept both canonical `$`-fields and the SDK colon-alias shape.
549
- const adapter = String(data.$adapter ?? data.adapter ?? '');
550
- const endpointId = String(data.$endpoint ?? data.endpointId ?? '');
526
+ const data = message;
527
+ const adapter = String(data.$adapter ?? '');
528
+ const endpointId = String(data.$endpoint ?? '');
551
529
  if (!adapter || !endpointId) {
552
530
  emit({ requestId, error: '$adapter and $endpoint required' });
553
531
  return payloads;
@@ -573,14 +551,12 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
573
551
  }
574
552
  case 'endpoint.send_message': {
575
553
  try {
576
- const data = (message.data ?? {});
577
- // Accept both canonical `$`-fields and the SDK colon-alias shape
578
- // (`endpoint:sendMessage` → data.{adapter, endpointId, id, type, content, parent}).
579
- const adapter = String(data.$adapter ?? data.adapter ?? '');
580
- const endpointId = String(data.$endpoint ?? data.endpointId ?? '');
581
- const channelId = String(data.$channel_id ?? data.id ?? '');
582
- const channelType = String(data.$channel_type ?? data.type ?? '');
583
- const content = data.$content !== undefined ? data.$content : data.content;
554
+ const data = message;
555
+ const adapter = String(data.$adapter ?? '');
556
+ const endpointId = String(data.$endpoint ?? '');
557
+ const channelId = String(data.$channel_id ?? '');
558
+ const channelType = String(data.$channel_type ?? '');
559
+ const content = data.$content;
584
560
  // channelType is optional: ImRuntime tolerates an empty value and
585
561
  // targets the bare channel id (parity with the legacy RPC handler).
586
562
  if (!adapter || !endpointId || !channelId || content === undefined) {
@@ -600,10 +576,10 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
600
576
  channelId,
601
577
  channelType,
602
578
  content,
603
- parent: (data.$parent ?? data.parent),
579
+ parent: data.$parent,
604
580
  });
605
581
  // Legacy contract: { message_id }. Keep messageId for new callers.
606
- emit({ requestId, data: { message_id: result.messageId, messageId: result.messageId } });
582
+ emit({ requestId, data: endpointSendResult(result.messageId) });
607
583
  }
608
584
  catch (error) {
609
585
  emit({
@@ -619,6 +595,7 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
619
595
  emit({ requestId, error: 'Process restart is not configured' });
620
596
  return payloads;
621
597
  }
598
+ ctx.publishEvent?.('system:restarting', { timestamp: Date.now() });
622
599
  emit({ requestId, data: { success: true, message: '正在重启...' } });
623
600
  // Delay so the HTTP response can flush (parity with legacy host-api).
624
601
  setTimeout(() => {
package/lib/index.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export { createHttpHost, httpHostToken, REMOTE_CONSOLE_ORIGIN, type HttpHandler, type HttpHost, type HttpHostAddress, type HttpHostOptions, type HttpRouteRegistration, type WsConnection, type WsHandle, } from './http-host.js';
2
2
  export { TokenRegistry, extractBearerToken, isDemoWebSocketPath, type AuthScope, type ScopedTokenConfig, type TokenRegistryConfig, } from './token-registry.js';
3
3
  export { timingSafeEqualString } from './timing-safe-equal.js';
4
+ export { createConsoleEventHub, type ConsoleEventHub, } from './console-events.js';
4
5
  export { buildOpenApiDocument, patternToOpenApiPath, routeRequiresBearerAuth, type BuildOpenApiOptions, type ListedRoute, type OpenApiParameter, type RouteMeta, } from './openapi.js';
5
6
  export { HttpBodyError, readJsonBody, } from './json-body.js';
6
7
  export { dispatchRuntimeConsoleRpc, isDemoHttpAllowed, pickRpcReply, type RuntimeConsolePage, type RuntimeConsoleRpcContext, type RuntimeConsoleRpcMessage, type RuntimeConsoleRpcReply, type RuntimeEndpointSendInput, type RuntimeEndpointSummary, } from './console-rpc.js';
7
8
  export { buildProjectFileTree, FILE_MANAGER_ALLOWED, FILE_MANAGER_BLOCKED, isProjectPathAllowed, listEnvFiles, readProjectFile, saveProjectFile, type FileTreeNode, } from './project-files.js';
8
9
  export { registerConsoleRestPages, type ConsoleAgentIntrospection, type ConsoleAgentRuntime, type ConsoleAgentSessionTree, type ConsoleRestCtx, type ConsoleRestPagesOptions, } from './console-rest-pages.js';
10
+ export { dispatchExtendedConsoleRpc, type ConsoleRpcExtendedCtx, type ConsoleScheduleEngine, type ExtendedRpcResult, } from './console-rpc-extended.js';
package/lib/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  export { createHttpHost, httpHostToken, REMOTE_CONSOLE_ORIGIN, } from './http-host.js';
2
2
  export { TokenRegistry, extractBearerToken, isDemoWebSocketPath, } from './token-registry.js';
3
3
  export { timingSafeEqualString } from './timing-safe-equal.js';
4
+ export { createConsoleEventHub, } from './console-events.js';
4
5
  export { buildOpenApiDocument, patternToOpenApiPath, routeRequiresBearerAuth, } from './openapi.js';
5
6
  export { HttpBodyError, readJsonBody, } from './json-body.js';
6
7
  export { dispatchRuntimeConsoleRpc, isDemoHttpAllowed, pickRpcReply, } from './console-rpc.js';
7
8
  export { buildProjectFileTree, FILE_MANAGER_ALLOWED, FILE_MANAGER_BLOCKED, isProjectPathAllowed, listEnvFiles, readProjectFile, saveProjectFile, } from './project-files.js';
8
9
  export { registerConsoleRestPages, } from './console-rest-pages.js';
10
+ export { dispatchExtendedConsoleRpc, } from './console-rpc-extended.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/host-http",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Minimal HTTP and WebSocket host for Plugin Runtime (no Koa)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -18,8 +18,9 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "ws": "^8.21.0",
21
- "@zhin.js/logger": "1.0.74",
22
- "@zhin.js/plugin-runtime": "1.0.0"
21
+ "@zhin.js/console-protocol": "1.1.0",
22
+ "@zhin.js/logger": "1.0.75",
23
+ "@zhin.js/plugin-runtime": "1.1.0"
23
24
  },
24
25
  "devDependencies": {
25
26
  "@types/node": "^26.1.0",
@@ -0,0 +1,47 @@
1
+ import type { ServerResponse } from 'node:http';
2
+
3
+ /**
4
+ * Console 实时事件枢纽(Plugin Runtime Host 的 `/api/events` SSE 事件源)。
5
+ * 单进程内 fan-out:`publish` 向所有已订阅的 SSE response 写帧;
6
+ * write 失败或连接断开自动摘除订阅。
7
+ */
8
+ export interface ConsoleEventHub {
9
+ publish(type: string, data: unknown): void;
10
+ /** 订阅事件流;返回 unsubscribe。连接断开时也会自动摘除。 */
11
+ subscribe(response: ServerResponse): () => void;
12
+ readonly subscriberCount: number;
13
+ }
14
+
15
+ export function createConsoleEventHub(): ConsoleEventHub {
16
+ let nextId = 0;
17
+ const subscribers = new Set<ServerResponse>();
18
+
19
+ const remove = (response: ServerResponse): void => {
20
+ subscribers.delete(response);
21
+ };
22
+
23
+ return {
24
+ get subscriberCount() {
25
+ return subscribers.size;
26
+ },
27
+ publish(type, data) {
28
+ nextId += 1;
29
+ const frame = `id: ${nextId}\nevent: ${type}\ndata: ${JSON.stringify(data)}\n\n`;
30
+ for (const response of [...subscribers]) {
31
+ try {
32
+ response.write(frame, (error) => {
33
+ if (error) remove(response);
34
+ });
35
+ } catch {
36
+ remove(response);
37
+ }
38
+ }
39
+ },
40
+ subscribe(response) {
41
+ subscribers.add(response);
42
+ response.once('close', () => remove(response));
43
+ response.once('error', () => remove(response));
44
+ return () => remove(response);
45
+ },
46
+ };
47
+ }