@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,5 +1,11 @@
1
1
  import type { AuthScope } from './token-registry.js';
2
2
  import type { DatabaseHostConsole } from '@zhin.js/plugin-runtime';
3
+ import {
4
+ type ConsoleEndpointSummary,
5
+ assertDemoConsoleRpcAllowed,
6
+ endpointSendResult,
7
+ normalizeConsoleRpcMessage,
8
+ } from '@zhin.js/console-protocol';
3
9
  import { dispatchExtendedConsoleRpc, type ConsoleRpcExtendedCtx } from './console-rpc-extended.js';
4
10
 
5
11
  export type RuntimeConsoleRpcMessage = {
@@ -57,21 +63,19 @@ export type RuntimeConsoleRpcContext = {
57
63
  database?: DatabaseHostConsole;
58
64
  /** Full-scope: request process restart (CLI daemon watches exit code). */
59
65
  requestRestart?(): Promise<void> | void;
66
+ /**
67
+ * Optional console event sink (`/api/events` SSE fan-out). Called after
68
+ * successful mutations: `config:updated` / `system:restarting`.
69
+ */
70
+ publishEvent?(type: string, data: unknown): void;
60
71
  /** Extended RPC surface(cron/schedule、endpoint 社交/inbox),fullScope 由 authScope 推导。 */
61
72
  extended?: Omit<ConsoleRpcExtendedCtx, 'fullScope'>;
62
73
  };
63
74
 
64
- export type RuntimeEndpointPhase =
65
- 'pending' | 'starting' | 'online' | 'failed' | 'unconfigured';
75
+ export type RuntimeEndpointPhase = NonNullable<ConsoleEndpointSummary['phase']>;
66
76
 
67
- export type RuntimeEndpointSummary = {
68
- readonly name: string;
69
- readonly adapter: string;
70
- readonly connected: boolean;
71
- readonly status: 'online' | 'offline';
72
- readonly phase?: RuntimeEndpointPhase;
73
- readonly pendingLogin?: boolean;
74
- };
77
+ /** @deprecated Prefer `ConsoleEndpointSummary` from `@zhin.js/console-protocol`. */
78
+ export type RuntimeEndpointSummary = ConsoleEndpointSummary;
75
79
 
76
80
  export type RuntimeDatabaseInfo = {
77
81
  readonly dialect: string | null;
@@ -88,47 +92,6 @@ export type RuntimeEndpointSendInput = {
88
92
  readonly parent?: { type?: string; id?: string; name?: string };
89
93
  };
90
94
 
91
- /**
92
- * Frontend SDK (@zhin.js/client) calls colon-named aliases; normalize to the
93
- * canonical dot names before scope checks and dispatch. Alias parameter shapes
94
- * (`data.{adapter, endpointId, id, type, content, parent}`) are mapped onto the
95
- * `$`-prefixed fields inside each endpoint case.
96
- */
97
- const RPC_TYPE_ALIASES: Readonly<Record<string, string>> = Object.freeze({
98
- 'endpoint:list': 'endpoint.list',
99
- 'endpoint:info': 'endpoint.info',
100
- 'endpoint:sendMessage': 'endpoint.send_message',
101
- });
102
-
103
- /** ADR 0016 demo allowlist subset available on Plugin Runtime Host. */
104
- const DEMO_RPC_ALLOWLIST = new Set([
105
- 'ping',
106
- 'entries:get',
107
- 'pages:list',
108
- 'config:get',
109
- 'config:get-all',
110
- 'config:get-yaml',
111
- 'schema:get',
112
- 'schema:get-all',
113
- 'endpoint.list',
114
- 'endpoint.info',
115
- 'endpoint.send_message',
116
- ]);
117
-
118
- const DEMO_RPC_WRITE_BLOCKLIST = new Set([
119
- 'config:set',
120
- 'config:save-yaml',
121
- 'files:save',
122
- 'env:save',
123
- 'db:insert',
124
- 'db:update',
125
- 'db:delete',
126
- 'db:drop-table',
127
- 'db:kv:set',
128
- 'db:kv:delete',
129
- 'system:restart',
130
- ]);
131
-
132
95
  export function pickRpcReply(
133
96
  message: RuntimeConsoleRpcMessage,
134
97
  payloads: readonly RuntimeConsoleRpcReply[],
@@ -149,14 +112,23 @@ export async function dispatchRuntimeConsoleRpc(
149
112
  const emit = (payload: RuntimeConsoleRpcReply) => {
150
113
  payloads.push(payload);
151
114
  };
152
- const type = RPC_TYPE_ALIASES[String(message.type ?? '')] ?? String(message.type ?? '');
115
+ message = normalizeConsoleRpcMessage(message);
116
+ const type = String(message.type ?? '');
153
117
  const requestId = message.requestId as number | string | undefined;
154
118
 
155
- // Extended RPC surface (cron/schedule、endpoint 社交/inbox) — 自带 scope 矩阵
119
+ if (ctx.authScope === 'demo') {
120
+ const denied = assertDemoConsoleRpcAllowed(type);
121
+ if (denied) {
122
+ emit({ requestId, error: denied });
123
+ return payloads;
124
+ }
125
+ }
126
+
127
+ // Extended RPC surface (cron/schedule、endpoint 社交/inbox)
156
128
  if (ctx.extended) {
157
129
  const extended = await dispatchExtendedConsoleRpc(
158
130
  type,
159
- message.data as Record<string, unknown> | undefined,
131
+ message as Record<string, unknown>,
160
132
  { ...ctx.extended, fullScope: ctx.authScope === 'full' },
161
133
  );
162
134
  if (extended !== undefined) {
@@ -167,11 +139,6 @@ export async function dispatchRuntimeConsoleRpc(
167
139
  }
168
140
  }
169
141
 
170
- if (ctx.authScope === 'demo' && (DEMO_RPC_WRITE_BLOCKLIST.has(type) || !DEMO_RPC_ALLOWLIST.has(type))) {
171
- emit({ requestId, error: `Demo scope: RPC "${type}" is forbidden` });
172
- return payloads;
173
- }
174
-
175
142
  switch (type) {
176
143
  case 'ping':
177
144
  emit({ type: 'pong', requestId });
@@ -224,7 +191,9 @@ export async function dispatchRuntimeConsoleRpc(
224
191
  }
225
192
  case 'config:get': {
226
193
  try {
227
- const key = String(message.key ?? message.pluginName ?? '');
194
+ // 兼容 console UI 的 data:{plugin} / data:{key} 形状
195
+ const data = (message.data ?? {}) as Record<string, unknown>;
196
+ const key = String(message.key ?? message.pluginName ?? data.pluginName ?? data.plugin ?? data.key ?? '');
228
197
  const document = ctx.readConfigDocument ? await ctx.readConfigDocument() : {};
229
198
  emit({ requestId, data: key ? document[key] : document });
230
199
  } catch (error) {
@@ -247,6 +216,7 @@ export async function dispatchRuntimeConsoleRpc(
247
216
  return payloads;
248
217
  }
249
218
  await ctx.writeConfigYaml(yaml);
219
+ ctx.publishEvent?.('config:updated', { pluginName: null, keys: [] });
250
220
  emit({
251
221
  requestId,
252
222
  data: { success: true, message: '配置已保存,需重启生效' },
@@ -271,6 +241,12 @@ export async function dispatchRuntimeConsoleRpc(
271
241
  return payloads;
272
242
  }
273
243
  const result = await ctx.setConfigKey(pluginName, message.data);
244
+ ctx.publishEvent?.('config:updated', {
245
+ pluginName,
246
+ keys: message.data && typeof message.data === 'object' && !Array.isArray(message.data)
247
+ ? Object.keys(message.data as Record<string, unknown>)
248
+ : [],
249
+ });
274
250
  emit({
275
251
  requestId,
276
252
  data: {
@@ -409,7 +385,10 @@ export async function dispatchRuntimeConsoleRpc(
409
385
  }
410
386
  case 'schema:get': {
411
387
  try {
412
- const pluginName = typeof message.pluginName === 'string' ? message.pluginName : undefined;
388
+ // 兼容 console UI 的 data:{plugin} / data:{pluginName} 形状(SDK 发顶层 pluginName)
389
+ const data = (message.data ?? {}) as Record<string, unknown>;
390
+ const candidate = message.pluginName ?? data.pluginName ?? data.plugin;
391
+ const pluginName = typeof candidate === 'string' ? candidate : undefined;
413
392
  const schema = ctx.getSchema ? await ctx.getSchema(pluginName) : null;
414
393
  emit({ requestId, data: schema });
415
394
  } catch (error) {
@@ -613,10 +592,9 @@ export async function dispatchRuntimeConsoleRpc(
613
592
  }
614
593
  case 'endpoint.info': {
615
594
  try {
616
- const data = (message.data ?? {}) as Record<string, unknown>;
617
- // Accept both canonical `$`-fields and the SDK colon-alias shape.
618
- const adapter = String(data.$adapter ?? data.adapter ?? '');
619
- const endpointId = String(data.$endpoint ?? data.endpointId ?? '');
595
+ const data = message as Record<string, unknown>;
596
+ const adapter = String(data.$adapter ?? '');
597
+ const endpointId = String(data.$endpoint ?? '');
620
598
  if (!adapter || !endpointId) {
621
599
  emit({ requestId, error: '$adapter and $endpoint required' });
622
600
  return payloads;
@@ -641,14 +619,12 @@ export async function dispatchRuntimeConsoleRpc(
641
619
  }
642
620
  case 'endpoint.send_message': {
643
621
  try {
644
- const data = (message.data ?? {}) as Record<string, unknown>;
645
- // Accept both canonical `$`-fields and the SDK colon-alias shape
646
- // (`endpoint:sendMessage` → data.{adapter, endpointId, id, type, content, parent}).
647
- const adapter = String(data.$adapter ?? data.adapter ?? '');
648
- const endpointId = String(data.$endpoint ?? data.endpointId ?? '');
649
- const channelId = String(data.$channel_id ?? data.id ?? '');
650
- const channelType = String(data.$channel_type ?? data.type ?? '');
651
- const content = data.$content !== undefined ? data.$content : data.content;
622
+ const data = message as Record<string, unknown>;
623
+ const adapter = String(data.$adapter ?? '');
624
+ const endpointId = String(data.$endpoint ?? '');
625
+ const channelId = String(data.$channel_id ?? '');
626
+ const channelType = String(data.$channel_type ?? '');
627
+ const content = data.$content;
652
628
  // channelType is optional: ImRuntime tolerates an empty value and
653
629
  // targets the bare channel id (parity with the legacy RPC handler).
654
630
  if (!adapter || !endpointId || !channelId || content === undefined) {
@@ -668,11 +644,11 @@ export async function dispatchRuntimeConsoleRpc(
668
644
  channelId,
669
645
  channelType,
670
646
  content,
671
- parent: (data.$parent ?? data.parent) as
647
+ parent: data.$parent as
672
648
  { type?: string; id?: string; name?: string } | undefined,
673
649
  });
674
650
  // Legacy contract: { message_id }. Keep messageId for new callers.
675
- emit({ requestId, data: { message_id: result.messageId, messageId: result.messageId } });
651
+ emit({ requestId, data: endpointSendResult(result.messageId) });
676
652
  } catch (error) {
677
653
  emit({
678
654
  requestId,
@@ -687,6 +663,7 @@ export async function dispatchRuntimeConsoleRpc(
687
663
  emit({ requestId, error: 'Process restart is not configured' });
688
664
  return payloads;
689
665
  }
666
+ ctx.publishEvent?.('system:restarting', { timestamp: Date.now() });
690
667
  emit({ requestId, data: { success: true, message: '正在重启...' } });
691
668
  // Delay so the HTTP response can flush (parity with legacy host-api).
692
669
  setTimeout(() => {
package/src/index.ts CHANGED
@@ -19,6 +19,10 @@ export {
19
19
  type TokenRegistryConfig,
20
20
  } from './token-registry.js';
21
21
  export { timingSafeEqualString } from './timing-safe-equal.js';
22
+ export {
23
+ createConsoleEventHub,
24
+ type ConsoleEventHub,
25
+ } from './console-events.js';
22
26
  export {
23
27
  buildOpenApiDocument,
24
28
  patternToOpenApiPath,
@@ -61,3 +65,9 @@ export {
61
65
  type ConsoleRestCtx,
62
66
  type ConsoleRestPagesOptions,
63
67
  } from './console-rest-pages.js';
68
+ export {
69
+ dispatchExtendedConsoleRpc,
70
+ type ConsoleRpcExtendedCtx,
71
+ type ConsoleScheduleEngine,
72
+ type ExtendedRpcResult,
73
+ } from './console-rpc-extended.js';