@zhin.js/host-http 1.1.17 → 1.2.0
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/README.md +60 -21
- package/lib/access-control.d.ts +127 -0
- package/lib/access-control.js +148 -0
- package/lib/addresses.d.ts +9 -0
- package/lib/addresses.js +9 -0
- package/lib/bridge/bridged-host.d.ts +43 -0
- package/lib/bridge/bridged-host.js +497 -0
- package/lib/bridge/control-channel.d.ts +59 -0
- package/lib/bridge/control-channel.js +131 -0
- package/lib/bridge/flow.d.ts +58 -0
- package/lib/bridge/flow.js +188 -0
- package/lib/bridge/index.d.ts +6 -0
- package/lib/bridge/index.js +6 -0
- package/lib/bridge/instance-transport.d.ts +112 -0
- package/lib/bridge/instance-transport.js +532 -0
- package/lib/bridge/protocol.d.ts +242 -0
- package/lib/bridge/protocol.js +369 -0
- package/lib/console-events.d.ts +5 -3
- package/lib/console-events.js +53 -27
- package/lib/console-plugin-rpc.d.ts +5 -1
- package/lib/console-plugin-rpc.js +5 -40
- package/lib/console-rest-pages-helpers.d.ts +4 -5
- package/lib/console-rest-pages-helpers.js +62 -79
- package/lib/console-rest-pages.js +80 -65
- package/lib/console-rpc-extended/dispatcher.js +1 -6
- package/lib/console-rpc-handlers.d.ts +0 -2
- package/lib/console-rpc-handlers.js +2 -40
- package/lib/console-rpc.d.ts +6 -14
- package/lib/console-rpc.js +6 -6
- package/lib/host-installer.d.ts +6 -3
- package/lib/host-installer.js +10 -40
- package/lib/host-options.d.ts +47 -0
- package/lib/host-options.js +1 -0
- package/lib/http-host.d.ts +124 -48
- package/lib/http-host.js +319 -332
- package/lib/index.d.ts +10 -5
- package/lib/index.js +9 -5
- package/lib/json-body.d.ts +1 -2
- package/lib/json-body.js +1 -2
- package/lib/marketplace-registry.d.ts +17 -0
- package/lib/marketplace-registry.js +52 -0
- package/lib/node/index.d.ts +10 -0
- package/lib/node/index.js +10 -0
- package/lib/{listener-group.d.ts → node/listener-group.d.ts} +2 -3
- package/lib/{listener-group.js → node/listener-group.js} +6 -10
- package/lib/node/listener.d.ts +26 -0
- package/lib/node/listener.js +231 -0
- package/lib/node/node-http.d.ts +38 -0
- package/lib/node/node-http.js +272 -0
- package/lib/node/server.d.ts +13 -0
- package/lib/node/server.js +64 -0
- package/lib/protocol-auth.d.ts +24 -0
- package/lib/protocol-auth.js +48 -0
- package/lib/public-api.d.ts +1 -1
- package/lib/token-registry.d.ts +54 -36
- package/lib/token-registry.js +133 -126
- package/lib/trust-proxy.d.ts +22 -0
- package/lib/trust-proxy.js +168 -0
- package/package.json +14 -6
- package/src/access-control.ts +255 -0
- package/src/addresses.ts +16 -0
- package/src/bridge/bridged-host.ts +548 -0
- package/src/bridge/control-channel.ts +173 -0
- package/src/bridge/flow.ts +208 -0
- package/src/bridge/index.ts +31 -0
- package/src/bridge/instance-transport.ts +627 -0
- package/src/bridge/protocol.ts +574 -0
- package/src/console-events.ts +59 -28
- package/src/console-plugin-rpc.ts +5 -51
- package/src/console-rest-pages-helpers.ts +60 -82
- package/src/console-rest-pages.ts +87 -67
- package/src/console-rpc-extended/dispatcher.ts +1 -6
- package/src/console-rpc-handlers.ts +2 -31
- package/src/console-rpc.ts +10 -19
- package/src/host-installer.ts +12 -44
- package/src/host-options.ts +47 -0
- package/src/http-host.ts +467 -421
- package/src/index.ts +55 -13
- package/src/json-body.ts +7 -2
- package/src/marketplace-registry.ts +62 -0
- package/src/node/index.ts +21 -0
- package/src/{listener-group.ts → node/listener-group.ts} +13 -24
- package/src/node/listener.ts +265 -0
- package/src/node/node-http.ts +263 -0
- package/src/node/server.ts +82 -0
- package/src/protocol-auth.ts +52 -0
- package/src/public-api.ts +2 -0
- package/src/token-registry.ts +143 -146
- package/src/trust-proxy.ts +178 -0
|
@@ -4,24 +4,29 @@
|
|
|
4
4
|
* 统一提供日志、市场、Runtime snapshot 内省和 Agent session tree 页面。
|
|
5
5
|
*
|
|
6
6
|
* 数据源不可用时降级:读操作返回空数组 + `note` 说明,session tree 返回 503;
|
|
7
|
-
* 写操作要求 `ctx.fullScope
|
|
7
|
+
* 写操作要求 `ctx.fullScope`,否则 403。
|
|
8
8
|
*/
|
|
9
9
|
import { readFile } from 'node:fs/promises';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import type { HttpHost } from './http-host.js';
|
|
12
12
|
import { readJsonBody } from './json-body.js';
|
|
13
|
-
import {
|
|
13
|
+
import { jsonResponse } from '@zhin.js/host-http-contract';
|
|
14
|
+
import {
|
|
15
|
+
MarketplaceDetailCache,
|
|
16
|
+
MarketplaceRegistry,
|
|
17
|
+
isValidNpmPackageName,
|
|
18
|
+
} from './marketplace-registry.js';
|
|
14
19
|
import type { ConsoleMessageEventData } from '@zhin.js/console-protocol';
|
|
15
20
|
import {
|
|
16
21
|
clampPositiveInt,
|
|
17
22
|
decodePathParam,
|
|
23
|
+
logInternalError,
|
|
18
24
|
normalizeBase,
|
|
19
25
|
parsePositiveInt,
|
|
20
26
|
registerAgentSessionRoutes,
|
|
21
27
|
registerIntrospectionRoutes,
|
|
22
28
|
requireWriteScope,
|
|
23
29
|
trimTrailingSlashes,
|
|
24
|
-
writeJson,
|
|
25
30
|
} from './console-rest-pages-helpers.js';
|
|
26
31
|
|
|
27
32
|
/** 与 basic/cli 接线方对齐的上下文约定。 */
|
|
@@ -135,6 +140,8 @@ type LogRow = {
|
|
|
135
140
|
};
|
|
136
141
|
|
|
137
142
|
const PLUGINS_CACHE_TTL = 5 * 60 * 1000;
|
|
143
|
+
const PACKAGE_DETAIL_CACHE_TTL = 5 * 60 * 1000;
|
|
144
|
+
const PACKAGE_DETAIL_CACHE_MAX_ENTRIES = 64;
|
|
138
145
|
|
|
139
146
|
/**
|
|
140
147
|
* 注册 Console 页面 REST 路由。返回注销函数(注销全部已注册路由)。
|
|
@@ -188,7 +195,7 @@ function registerLogsRoutes(
|
|
|
188
195
|
};
|
|
189
196
|
const unavailableNote = 'SystemLog 模型不可用(Database 未启动或未注册 DatabaseLogTransport)';
|
|
190
197
|
|
|
191
|
-
route('GET', `${base}/logs`, async (_request,
|
|
198
|
+
route('GET', `${base}/logs`, async (_request, { url }) => {
|
|
192
199
|
const pageSize = clampPositiveInt(parsePositiveInt(url.searchParams.get('pageSize') ?? url.searchParams.get('limit'), 100), 100, 1000);
|
|
193
200
|
const requestedPage = clampPositiveInt(parsePositiveInt(url.searchParams.get('page'), 1), 1, Number.MAX_SAFE_INTEGER);
|
|
194
201
|
const level = url.searchParams.get('level');
|
|
@@ -200,8 +207,7 @@ function registerLogsRoutes(
|
|
|
200
207
|
const where = { ...baseWhere, ...(source ? { source } : {}) };
|
|
201
208
|
const LogModel = getModel();
|
|
202
209
|
if (!LogModel) {
|
|
203
|
-
|
|
204
|
-
return;
|
|
210
|
+
return jsonResponse(200, { success: true, data: [], total: 0, page: 1, pageSize, totalPages: 1, sources: [], note: unavailableNote });
|
|
205
211
|
}
|
|
206
212
|
// Database $like has no portable ESCAPE contract. Keep q literal (including
|
|
207
213
|
// %/_ and Unicode) and filter actual retained rows on the Host, not wildcard SQL.
|
|
@@ -228,7 +234,7 @@ function registerLogsRoutes(
|
|
|
228
234
|
if (!name || !matchesQuery(row)) continue;
|
|
229
235
|
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
230
236
|
}
|
|
231
|
-
|
|
237
|
+
return jsonResponse(200, {
|
|
232
238
|
success: true,
|
|
233
239
|
data: logs.map((log) => ({
|
|
234
240
|
id: log.id,
|
|
@@ -243,19 +249,20 @@ function registerLogsRoutes(
|
|
|
243
249
|
});
|
|
244
250
|
}, { summary: 'List system logs', tags: ['console', 'logs'] });
|
|
245
251
|
|
|
246
|
-
route('DELETE', `${base}/logs`, async (
|
|
247
|
-
|
|
252
|
+
route('DELETE', `${base}/logs`, async () => {
|
|
253
|
+
const denied = requireWriteScope(ctx);
|
|
254
|
+
if (denied) return denied;
|
|
248
255
|
const LogModel = getModel();
|
|
249
256
|
if (LogModel) {
|
|
250
257
|
await LogModel.delete({});
|
|
251
258
|
}
|
|
252
|
-
|
|
259
|
+
return jsonResponse(200, { success: true, message: '日志已清空' });
|
|
253
260
|
}, { summary: 'Clear system logs', tags: ['console', 'logs'] });
|
|
254
261
|
|
|
255
|
-
route('GET', `${base}/logs/stats`, async (
|
|
262
|
+
route('GET', `${base}/logs/stats`, async () => {
|
|
256
263
|
const LogModel = getModel();
|
|
257
264
|
if (!LogModel) {
|
|
258
|
-
|
|
265
|
+
return jsonResponse(200, {
|
|
259
266
|
success: true,
|
|
260
267
|
data: {
|
|
261
268
|
total: 0,
|
|
@@ -264,7 +271,6 @@ function registerLogsRoutes(
|
|
|
264
271
|
},
|
|
265
272
|
note: unavailableNote,
|
|
266
273
|
});
|
|
267
|
-
return;
|
|
268
274
|
}
|
|
269
275
|
|
|
270
276
|
// 计数走 DB 侧聚合(count);模型未实现 count 时降级为 select('id') 只取主键列
|
|
@@ -293,23 +299,23 @@ function registerLogsRoutes(
|
|
|
293
299
|
: (oldestLog[0].timestamp ?? null)
|
|
294
300
|
: null;
|
|
295
301
|
|
|
296
|
-
|
|
302
|
+
return jsonResponse(200, {
|
|
297
303
|
success: true,
|
|
298
304
|
data: { total, byLevel: levelCounts, oldestTimestamp },
|
|
299
305
|
});
|
|
300
306
|
}, { summary: 'System log stats', tags: ['console', 'logs'] });
|
|
301
307
|
|
|
302
|
-
route('POST', `${base}/logs/cleanup`, async (request
|
|
303
|
-
|
|
308
|
+
route('POST', `${base}/logs/cleanup`, async (request) => {
|
|
309
|
+
const denied = requireWriteScope(ctx);
|
|
310
|
+
if (denied) return denied;
|
|
304
311
|
const LogModel = getModel();
|
|
305
312
|
if (!LogModel) {
|
|
306
|
-
|
|
313
|
+
return jsonResponse(200, {
|
|
307
314
|
success: true,
|
|
308
315
|
message: '已清理 0 条日志',
|
|
309
316
|
deletedCount: 0,
|
|
310
317
|
note: unavailableNote,
|
|
311
318
|
});
|
|
312
|
-
return;
|
|
313
319
|
}
|
|
314
320
|
|
|
315
321
|
const body = (await readJsonBody(request) ?? {}) as {
|
|
@@ -342,7 +348,7 @@ function registerLogsRoutes(
|
|
|
342
348
|
}
|
|
343
349
|
}
|
|
344
350
|
|
|
345
|
-
|
|
351
|
+
return jsonResponse(200, {
|
|
346
352
|
success: true,
|
|
347
353
|
message: `已清理 ${deletedCount} 条日志`,
|
|
348
354
|
deletedCount,
|
|
@@ -368,7 +374,47 @@ function registerMarketplaceRoutes(
|
|
|
368
374
|
ttlMs: PLUGINS_CACHE_TTL,
|
|
369
375
|
});
|
|
370
376
|
|
|
371
|
-
route
|
|
377
|
+
// The detail route is unauthenticated: cache the projected packument so repeated
|
|
378
|
+
// requests cannot be used to hammer the npm registry.
|
|
379
|
+
const details = new MarketplaceDetailCache<Record<string, unknown>>({
|
|
380
|
+
ttlMs: PACKAGE_DETAIL_CACHE_TTL,
|
|
381
|
+
maxEntries: PACKAGE_DETAIL_CACHE_MAX_ENTRIES,
|
|
382
|
+
load: async (pkgName) => {
|
|
383
|
+
const metaResp = await fetchFn(
|
|
384
|
+
`${npmRegistryUrl}/${encodeURIComponent(pkgName)}`,
|
|
385
|
+
{ signal: AbortSignal.timeout(30_000) },
|
|
386
|
+
);
|
|
387
|
+
if (!metaResp.ok) throw new Error(`Package not found: ${metaResp.status}`);
|
|
388
|
+
const meta = (await metaResp.json()) as Record<string, unknown>;
|
|
389
|
+
const latest = (meta['dist-tags'] as Record<string, string> | undefined)?.latest;
|
|
390
|
+
const versions = meta.versions as Record<string, Record<string, unknown>> | undefined;
|
|
391
|
+
const latestInfo = latest && versions ? versions[latest] : undefined;
|
|
392
|
+
const time = meta.time as Record<string, string> | undefined;
|
|
393
|
+
return {
|
|
394
|
+
name: meta.name,
|
|
395
|
+
version: latest,
|
|
396
|
+
description: meta.description || '',
|
|
397
|
+
readme: meta.readme || '',
|
|
398
|
+
license: meta.license || latestInfo?.license || '',
|
|
399
|
+
homepage: meta.homepage || latestInfo?.homepage || '',
|
|
400
|
+
repository:
|
|
401
|
+
(meta.repository as { url?: string } | undefined)?.url ||
|
|
402
|
+
(latestInfo?.repository as { url?: string } | undefined)?.url ||
|
|
403
|
+
'',
|
|
404
|
+
author:
|
|
405
|
+
typeof meta.author === 'string'
|
|
406
|
+
? meta.author
|
|
407
|
+
: (meta.author as { name?: string } | undefined)?.name || '',
|
|
408
|
+
keywords: (latestInfo?.keywords as string[]) || [],
|
|
409
|
+
engines: latestInfo?.engines || {},
|
|
410
|
+
peerDependencies: latestInfo?.peerDependencies || {},
|
|
411
|
+
versions: Object.keys(versions || {}),
|
|
412
|
+
lastPublish: (latest && time?.[latest]) || '',
|
|
413
|
+
};
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
route('GET', '/pub/marketplace/search', async (_request, { url }) => {
|
|
372
418
|
const q = url.searchParams.get('q') ?? url.searchParams.get('keyword') ?? '';
|
|
373
419
|
const pageNum = Math.max(1, parseInt(url.searchParams.get('page') ?? '1', 10) || 1);
|
|
374
420
|
const pageSize = Math.min(
|
|
@@ -413,7 +459,7 @@ function registerMarketplaceRoutes(
|
|
|
413
459
|
const total = plugins.length;
|
|
414
460
|
const start = (pageNum - 1) * pageSize;
|
|
415
461
|
const items = plugins.slice(start, start + pageSize);
|
|
416
|
-
|
|
462
|
+
return jsonResponse(200, {
|
|
417
463
|
success: true,
|
|
418
464
|
data: items,
|
|
419
465
|
total,
|
|
@@ -421,23 +467,26 @@ function registerMarketplaceRoutes(
|
|
|
421
467
|
size: pageSize,
|
|
422
468
|
});
|
|
423
469
|
} catch (err) {
|
|
424
|
-
|
|
470
|
+
logInternalError('marketplace_search_failed', err);
|
|
471
|
+
return jsonResponse(502, {
|
|
425
472
|
success: false,
|
|
426
|
-
error:
|
|
473
|
+
error: 'Search failed',
|
|
427
474
|
});
|
|
428
475
|
}
|
|
429
476
|
}, { summary: 'Search plugin marketplace', tags: ['pub', 'marketplace'] });
|
|
430
477
|
|
|
431
|
-
route('GET', '/pub/marketplace/detail/*', async (_request,
|
|
478
|
+
route('GET', '/pub/marketplace/detail/*', async (_request, { url }) => {
|
|
432
479
|
const prefix = '/pub/marketplace/detail/';
|
|
433
480
|
const rawName = url.pathname.startsWith(prefix)
|
|
434
481
|
? url.pathname.slice(prefix.length).replace(/\/+$/u, '')
|
|
435
482
|
: '';
|
|
436
483
|
if (!rawName) {
|
|
437
|
-
|
|
438
|
-
return;
|
|
484
|
+
return jsonResponse(404, { success: false, error: 'Package name is required' });
|
|
439
485
|
}
|
|
440
486
|
const pkgName = decodePathParam(rawName);
|
|
487
|
+
if (!isValidNpmPackageName(pkgName)) {
|
|
488
|
+
return jsonResponse(400, { success: false, error: 'Invalid package name' });
|
|
489
|
+
}
|
|
441
490
|
try {
|
|
442
491
|
let cachedDownloads = { weekly: 0, monthly: 0 };
|
|
443
492
|
try {
|
|
@@ -453,55 +502,25 @@ function registerMarketplaceRoutes(
|
|
|
453
502
|
/* ignore cache miss */
|
|
454
503
|
}
|
|
455
504
|
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
{ signal: AbortSignal.timeout(30_000) },
|
|
459
|
-
);
|
|
460
|
-
if (!metaResp.ok) throw new Error(`Package not found: ${metaResp.status}`);
|
|
461
|
-
const meta = (await metaResp.json()) as Record<string, unknown>;
|
|
462
|
-
const latest = (meta['dist-tags'] as Record<string, string> | undefined)?.latest;
|
|
463
|
-
const versions = meta.versions as Record<string, Record<string, unknown>> | undefined;
|
|
464
|
-
const latestInfo = latest && versions ? versions[latest] : undefined;
|
|
465
|
-
const time = meta.time as Record<string, string> | undefined;
|
|
466
|
-
writeJson(response, 200, {
|
|
505
|
+
const detail = await details.get(pkgName);
|
|
506
|
+
return jsonResponse(200, {
|
|
467
507
|
success: true,
|
|
468
|
-
data: {
|
|
469
|
-
name: meta.name,
|
|
470
|
-
version: latest,
|
|
471
|
-
description: meta.description || '',
|
|
472
|
-
readme: meta.readme || '',
|
|
473
|
-
license: meta.license || latestInfo?.license || '',
|
|
474
|
-
homepage: meta.homepage || latestInfo?.homepage || '',
|
|
475
|
-
repository:
|
|
476
|
-
(meta.repository as { url?: string } | undefined)?.url ||
|
|
477
|
-
(latestInfo?.repository as { url?: string } | undefined)?.url ||
|
|
478
|
-
'',
|
|
479
|
-
author:
|
|
480
|
-
typeof meta.author === 'string'
|
|
481
|
-
? meta.author
|
|
482
|
-
: (meta.author as { name?: string } | undefined)?.name || '',
|
|
483
|
-
keywords: (latestInfo?.keywords as string[]) || [],
|
|
484
|
-
engines: latestInfo?.engines || {},
|
|
485
|
-
peerDependencies: latestInfo?.peerDependencies || {},
|
|
486
|
-
downloads: cachedDownloads,
|
|
487
|
-
versions: Object.keys(versions || {}),
|
|
488
|
-
lastPublish: (latest && time?.[latest]) || '',
|
|
489
|
-
},
|
|
508
|
+
data: { ...detail, downloads: cachedDownloads },
|
|
490
509
|
});
|
|
491
510
|
} catch (err) {
|
|
492
|
-
|
|
511
|
+
logInternalError('marketplace_detail_failed', err);
|
|
512
|
+
return jsonResponse(502, {
|
|
493
513
|
success: false,
|
|
494
|
-
error:
|
|
514
|
+
error: 'Detail fetch failed',
|
|
495
515
|
});
|
|
496
516
|
}
|
|
497
517
|
}, { summary: 'Plugin marketplace detail', tags: ['pub', 'marketplace'] });
|
|
498
518
|
|
|
499
|
-
route('GET', `${base}/marketplace/updates`, async (
|
|
519
|
+
route('GET', `${base}/marketplace/updates`, async () => {
|
|
500
520
|
try {
|
|
501
521
|
const installed = await listInstalledPluginPackages(ctx.projectRoot);
|
|
502
522
|
if (!installed.length) {
|
|
503
|
-
|
|
504
|
-
return;
|
|
523
|
+
return jsonResponse(200, { success: true, data: [] });
|
|
505
524
|
}
|
|
506
525
|
const updates = await Promise.all(
|
|
507
526
|
installed.map(async ({ name, current }) => {
|
|
@@ -524,11 +543,12 @@ function registerMarketplaceRoutes(
|
|
|
524
543
|
}
|
|
525
544
|
}),
|
|
526
545
|
);
|
|
527
|
-
|
|
546
|
+
return jsonResponse(200, { success: true, data: updates.filter(Boolean) });
|
|
528
547
|
} catch (err) {
|
|
529
|
-
|
|
548
|
+
logInternalError('marketplace_updates_failed', err);
|
|
549
|
+
return jsonResponse(500, {
|
|
530
550
|
success: false,
|
|
531
|
-
error:
|
|
551
|
+
error: 'Update check failed',
|
|
532
552
|
});
|
|
533
553
|
}
|
|
534
554
|
}, { summary: 'Check installed plugin updates', tags: ['console', 'marketplace'] });
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { assertDemoConsoleRpcAllowed } from '@zhin.js/console-protocol';
|
|
2
1
|
import type { ConsoleRpcExtendedCtx, ExtendedRpcResult } from './contracts.js';
|
|
3
2
|
import { addCron, listSchedule, mutateCron, validateCron } from './schedule-rpc.js';
|
|
4
3
|
import {
|
|
@@ -16,13 +15,9 @@ export async function dispatchExtendedConsoleRpc(
|
|
|
16
15
|
data: Record<string, unknown> | undefined,
|
|
17
16
|
ctx: ConsoleRpcExtendedCtx,
|
|
18
17
|
): Promise<ExtendedRpcResult | undefined> {
|
|
18
|
+
// Callers authorize every message against the ADR 0049 permission map before dispatch.
|
|
19
19
|
const d = data ?? {};
|
|
20
20
|
|
|
21
|
-
if (!ctx.fullScope) {
|
|
22
|
-
const denied = assertDemoConsoleRpcAllowed(type);
|
|
23
|
-
if (denied) return { error: denied };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
21
|
switch (type) {
|
|
27
22
|
case 'schedule:list':
|
|
28
23
|
case 'cron:list':
|
|
@@ -241,7 +241,8 @@ export async function dispatchHostResourceConsoleRpc(
|
|
|
241
241
|
return true;
|
|
242
242
|
}
|
|
243
243
|
const tables = ctx.dbTables ? await ctx.dbTables() : [];
|
|
244
|
-
|
|
244
|
+
// Same `{ tables }` envelope as the database branch above; clients read `.tables`.
|
|
245
|
+
emit({ requestId, data: { tables } });
|
|
245
246
|
} catch (error) {
|
|
246
247
|
emit({
|
|
247
248
|
requestId,
|
|
@@ -560,26 +561,6 @@ export function errorReply(
|
|
|
560
561
|
return payloads;
|
|
561
562
|
}
|
|
562
563
|
|
|
563
|
-
/** HTTP paths under apiBase that demo scope may call (ADR 0016). */
|
|
564
|
-
export function isDemoHttpAllowed(method: string, pathname: string, apiBase: string): boolean {
|
|
565
|
-
const normalizedBase = apiBase.replace(/\/$/u, '') || '/api';
|
|
566
|
-
const m = method.toUpperCase();
|
|
567
|
-
// Each inference handler resolves a static authenticated personal binding before reading or mutating anything.
|
|
568
|
-
if (m === 'GET' && ['', '/owners', '/export', '/mutation', '/sessions', '/session'].some(path => pathname === `${normalizedBase}/agent/inference${path}`)) return true;
|
|
569
|
-
if (m === 'PUT' && pathname === `${normalizedBase}/agent/inference/session`) return true;
|
|
570
|
-
if (m === 'POST' && ['/plan', '/apply', '/key/prepare', '/discover', '/profile-schema'].some(path => pathname === `${normalizedBase}/agent/inference${path}`)) return true;
|
|
571
|
-
if (m === 'GET' && pathname === `${normalizedBase}/events`) return true;
|
|
572
|
-
if (m === 'GET' && pathname === `${normalizedBase}/events/history`) return true;
|
|
573
|
-
if (m === 'GET' && DEMO_INTROSPECTION_PATHS.has(pathname.slice(normalizedBase.length))) return true;
|
|
574
|
-
if (m === 'POST' && pathname === `${normalizedBase}/console/request`) return true;
|
|
575
|
-
// Read-only dashboard / plugin inspection routes (legacy host-api parity).
|
|
576
|
-
if (m === 'GET' && pathname === `${normalizedBase}/system/status`) return true;
|
|
577
|
-
if (m === 'GET' && pathname === `${normalizedBase}/stats`) return true;
|
|
578
|
-
if (m === 'GET' && (pathname === `${normalizedBase}/plugins`
|
|
579
|
-
|| pathname.startsWith(`${normalizedBase}/plugins/`))) return true;
|
|
580
|
-
return false;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
564
|
export function configurationSaveMessage(result: { restartRequired: boolean; revision?: string }
|
|
584
565
|
& ConsoleConfigApplicationStatus): string {
|
|
585
566
|
if (result.applicationError === 'superseded') return '配置已保存,但该次生效申请已被替代';
|
|
@@ -596,13 +577,3 @@ export function configurationSaveMessage(result: { restartRequired: boolean; rev
|
|
|
596
577
|
return result.restartRequired ? '配置已保存,需重启进程才能生效' : '配置已保存';
|
|
597
578
|
}
|
|
598
579
|
|
|
599
|
-
const DEMO_INTROSPECTION_PATHS: ReadonlySet<string> = new Set([
|
|
600
|
-
'/introspection/commands',
|
|
601
|
-
'/introspection/middlewares',
|
|
602
|
-
'/introspection/components',
|
|
603
|
-
'/introspection/endpoints',
|
|
604
|
-
'/introspection/bindings',
|
|
605
|
-
'/introspection/tools',
|
|
606
|
-
'/introspection/prompt-sections',
|
|
607
|
-
'/introspection/mcp',
|
|
608
|
-
]);
|
package/src/console-rpc.ts
CHANGED
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
type ConsoleConfigSource,
|
|
10
10
|
type ConsoleConfigExplanation,
|
|
11
11
|
type ConsoleEndpointSummary,
|
|
12
|
-
assertDemoConsoleRpcAllowed,
|
|
13
12
|
} from '@zhin.js/console-protocol';
|
|
13
|
+
import { authorizeConsoleRpc, type ConsolePrincipal } from './access-control.js';
|
|
14
14
|
import { dispatchExtendedConsoleRpc, type ConsoleRpcExtendedCtx } from './console-rpc-extended/index.js';
|
|
15
15
|
import { dispatchPluginConsoleRpc } from './console-plugin-rpc.js';
|
|
16
16
|
import {
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
invalidProjectPatchOperations,
|
|
20
20
|
} from './console-rpc-handlers.js';
|
|
21
21
|
|
|
22
|
-
export { isDemoHttpAllowed } from './console-rpc-handlers.js';
|
|
23
22
|
|
|
24
23
|
/**
|
|
25
24
|
* Console RPC 请求结构:未锚定 endpoint 的会话地址(结构对齐
|
|
@@ -135,21 +134,8 @@ export interface PluginManagementPort {
|
|
|
135
134
|
verificationPlan?(packageName: string): Promise<PluginVerificationPlan>;
|
|
136
135
|
verify?(packageName: string, checkId: string, expectedGeneration: number | null, expectedProjectRevision: string, expectedCheckRevision: string): Promise<PluginVerificationResult>;
|
|
137
136
|
planInstall(packageName: string): Promise<PluginInstallPlan>;
|
|
138
|
-
install?(packageName: string, expectedRevision?: string, expectedProjectRevision?: string): Promise<Readonly<{
|
|
139
|
-
readonly plan: PluginInstallPlan;
|
|
140
|
-
readonly restartRequired: boolean;
|
|
141
|
-
}>>;
|
|
142
137
|
planUninstall?(packageName: string): Promise<PluginUninstallPlan>;
|
|
143
|
-
uninstall?(packageName: string, expectedRevision?: string, expectedProjectRevision?: string): Promise<Readonly<{
|
|
144
|
-
readonly plan: PluginUninstallPlan;
|
|
145
|
-
readonly restartRequired: boolean;
|
|
146
|
-
}>>;
|
|
147
138
|
planUpdate?(packageName: string, targetVersion: string): Promise<PluginUpdatePlan>;
|
|
148
|
-
update?(packageName: string, targetVersion: string, expectedRevision?: string, expectedProjectRevision?: string): Promise<Readonly<{
|
|
149
|
-
readonly plan: PluginUpdatePlan;
|
|
150
|
-
readonly installedVersion: string;
|
|
151
|
-
readonly restartRequired: boolean;
|
|
152
|
-
}>>;
|
|
153
139
|
}
|
|
154
140
|
|
|
155
141
|
export interface PluginConfigValidation {
|
|
@@ -172,6 +158,11 @@ export type RuntimeConsolePage = {
|
|
|
172
158
|
|
|
173
159
|
export type RuntimeConsoleRpcContext = {
|
|
174
160
|
readonly authScope: AuthScope;
|
|
161
|
+
/**
|
|
162
|
+
* Caller principal. When set, every message is checked against the ADR 0049 permission map
|
|
163
|
+
* before dispatch (defence in depth; the route also checks).
|
|
164
|
+
*/
|
|
165
|
+
readonly principal?: ConsolePrincipal;
|
|
175
166
|
listPages(): Promise<readonly RuntimeConsolePage[]>;
|
|
176
167
|
/** Optional project config accessors for read-only config RPCs. */
|
|
177
168
|
readConfigSource?(): Promise<ConsoleConfigSource>;
|
|
@@ -282,10 +273,10 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
282
273
|
const type = String(message.type ?? '');
|
|
283
274
|
const requestId = message.requestId as number | string | undefined;
|
|
284
275
|
|
|
285
|
-
if (ctx.
|
|
286
|
-
const
|
|
287
|
-
if (
|
|
288
|
-
emit({ requestId, error:
|
|
276
|
+
if (ctx.principal) {
|
|
277
|
+
const decision = authorizeConsoleRpc(ctx.principal, message as Record<string, unknown>);
|
|
278
|
+
if (!decision.allowed) {
|
|
279
|
+
emit({ requestId, error: decision.error, code: decision.code });
|
|
289
280
|
return payloads;
|
|
290
281
|
}
|
|
291
282
|
}
|
package/src/host-installer.ts
CHANGED
|
@@ -2,18 +2,15 @@ import {
|
|
|
2
2
|
bindGenerationAdmission,
|
|
3
3
|
expandEnvironmentValue,
|
|
4
4
|
type ConfigDocumentPort,
|
|
5
|
+
type GenerationAdmissionBindable,
|
|
5
6
|
type RootResourceInstaller,
|
|
6
7
|
type RuntimeConfigDocument,
|
|
7
8
|
} from '@zhin.js/plugin-runtime';
|
|
8
9
|
import { readFile } from 'node:fs/promises';
|
|
9
10
|
import { isAbsolute, resolve } from 'node:path';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
type HttpHostOptions,
|
|
14
|
-
type ProcessHttpHost,
|
|
15
|
-
} from './http-host.js';
|
|
16
|
-
import type { ScopedTokenConfig } from './token-registry.js';
|
|
11
|
+
import { httpHostToken, type HttpHost } from './http-host.js';
|
|
12
|
+
import type { HttpHostOptions } from './host-options.js';
|
|
13
|
+
import { parseTrustProxy } from './trust-proxy.js';
|
|
17
14
|
|
|
18
15
|
export interface ResolvedHttpConfig extends HttpHostOptions {
|
|
19
16
|
/** Additional listeners that mirror the primary Host route surface. */
|
|
@@ -33,14 +30,15 @@ export async function resolveHttpConfig(
|
|
|
33
30
|
http,
|
|
34
31
|
(key) => envOverlay?.[key] ?? process.env[key],
|
|
35
32
|
) as Record<string, unknown>;
|
|
33
|
+
if (value.tokens !== undefined && value.tokens !== null) throw new Error(HTTP_TOKENS_REMOVED_MESSAGE);
|
|
36
34
|
const primary: HttpHostOptions = Object.freeze({
|
|
37
35
|
host: typeof value.host === 'string' ? value.host : undefined,
|
|
38
36
|
port: typeof value.port === 'number' ? value.port : undefined,
|
|
39
37
|
token: typeof value.token === 'string' ? value.token : undefined,
|
|
40
|
-
tokens: parseScopedTokens(value.tokens),
|
|
41
38
|
corsOrigins: injectDesktopWebviewOrigins(parseStringList(value.corsOrigins), envOverlay),
|
|
42
39
|
apiBase: typeof value.base === 'string' ? value.base : undefined,
|
|
43
40
|
tls: await parseTlsConfig(value.tls, projectRoot),
|
|
41
|
+
trustProxy: parseTrustProxy(value.trustProxy),
|
|
44
42
|
});
|
|
45
43
|
const configuredListeners = await parseAdditionalListeners(value.listeners, primary, projectRoot);
|
|
46
44
|
const listeners = await injectDesktopDeviceListener(
|
|
@@ -190,48 +188,18 @@ function resolveTlsPath(projectRoot: string, value: string): string {
|
|
|
190
188
|
return isAbsolute(value) ? value : resolve(projectRoot, value);
|
|
191
189
|
}
|
|
192
190
|
|
|
193
|
-
export function installHttpHost(host:
|
|
191
|
+
export function installHttpHost(host: HttpHost & GenerationAdmissionBindable<HttpHost>): RootResourceInstaller {
|
|
194
192
|
return ({ resources, admission }) => {
|
|
195
193
|
const port: HttpHost = host;
|
|
196
194
|
resources.provide(httpHostToken, bindGenerationAdmission(port, admission));
|
|
197
195
|
};
|
|
198
196
|
}
|
|
199
197
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const record = entry as Record<string, unknown>;
|
|
206
|
-
if (typeof record.token !== 'string' || !record.token) continue;
|
|
207
|
-
// `user` is an alias of `demo` (an ordinary Console user); anything but `full` is never the operator.
|
|
208
|
-
const scope = record.scope === 'full' ? 'full' : 'demo';
|
|
209
|
-
const principalId = typeof record.principalId === 'string' && record.principalId.trim() === record.principalId
|
|
210
|
-
&& record.principalId.length > 0
|
|
211
|
-
? record.principalId
|
|
212
|
-
: undefined;
|
|
213
|
-
const permissions = parseAccessList(record.permissions);
|
|
214
|
-
const roles = parseAccessList(record.roles);
|
|
215
|
-
tokens.push(Object.freeze({
|
|
216
|
-
token: record.token,
|
|
217
|
-
scope,
|
|
218
|
-
...(principalId ? { principalId } : {}),
|
|
219
|
-
...(permissions ? { permissions } : {}),
|
|
220
|
-
...(roles ? { roles } : {}),
|
|
221
|
-
}));
|
|
222
|
-
}
|
|
223
|
-
return tokens.length > 0 ? Object.freeze(tokens) : undefined;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/** Console permissions / roles of a scoped token; blank or non-string entries are dropped. */
|
|
227
|
-
function parseAccessList(value: unknown): readonly string[] | undefined {
|
|
228
|
-
if (!Array.isArray(value)) return undefined;
|
|
229
|
-
const result = value
|
|
230
|
-
.filter((item): item is string => typeof item === 'string')
|
|
231
|
-
.map((item) => item.trim())
|
|
232
|
-
.filter((item) => item.length > 0);
|
|
233
|
-
return result.length > 0 ? Object.freeze([...new Set(result)]) : undefined;
|
|
234
|
-
}
|
|
198
|
+
/** `http.tokens` was removed by ADR 0049; the Host refuses to start rather than ignore it. */
|
|
199
|
+
export const HTTP_TOKENS_REMOVED_MESSAGE = 'http.tokens 已移除(ADR 0049):请删除该配置,'
|
|
200
|
+
+ '改为 Console 用户(`zhin console user create`)与用户签发的 API 令牌;'
|
|
201
|
+
+ '`http.token` 只作为首次初始化令牌使用。'
|
|
202
|
+
+ ' / http.tokens was removed: delete it and use Console users and their API tokens instead.';
|
|
235
203
|
|
|
236
204
|
function parseStringList(value: unknown): readonly string[] | undefined {
|
|
237
205
|
if (!Array.isArray(value)) return undefined;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { TokenRegistry } from './token-registry.js';
|
|
2
|
+
import type { TrustProxySetting } from './trust-proxy.js';
|
|
3
|
+
|
|
4
|
+
export interface HttpHostAddress {
|
|
5
|
+
readonly host: string;
|
|
6
|
+
readonly port: number;
|
|
7
|
+
readonly protocol: 'http' | 'https';
|
|
8
|
+
readonly secure: boolean;
|
|
9
|
+
readonly origin: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type TlsMaterial = string | Uint8Array | ReadonlyArray<string | Uint8Array | { readonly pem: string | Uint8Array; readonly passphrase?: string }>;
|
|
13
|
+
|
|
14
|
+
export interface HttpHostTlsOptions {
|
|
15
|
+
readonly key: TlsMaterial;
|
|
16
|
+
readonly cert: TlsMaterial;
|
|
17
|
+
readonly ca?: string | Uint8Array | ReadonlyArray<string | Uint8Array>;
|
|
18
|
+
readonly passphrase?: string;
|
|
19
|
+
readonly minVersion?: 'TLSv1.3' | 'TLSv1.2' | 'TLSv1.1' | 'TLSv1';
|
|
20
|
+
readonly ciphers?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Listener configuration (`http.*`) for the in-process Node server and the gateway. */
|
|
24
|
+
export interface HttpHostOptions {
|
|
25
|
+
readonly host?: string;
|
|
26
|
+
readonly port?: number;
|
|
27
|
+
/** Bootstrap Bearer token (`http.token`), accepted only until the first owner exists. */
|
|
28
|
+
readonly token?: string;
|
|
29
|
+
/** CORS allowlist; always merged with Remote Console origin. */
|
|
30
|
+
readonly corsOrigins?: readonly string[];
|
|
31
|
+
/**
|
|
32
|
+
* Paths under this prefix require Bearer auth when a token registry is configured.
|
|
33
|
+
* Defaults to `/api`. `/pub/*`, Console shell, and page routes stay public.
|
|
34
|
+
*/
|
|
35
|
+
readonly apiBase?: string;
|
|
36
|
+
/** Extra HTTP path prefixes that skip auth even under `apiBase`. */
|
|
37
|
+
readonly authExemptPaths?: readonly string[];
|
|
38
|
+
/** Enables HTTPS and WSS on this listener. Certificate lifecycle belongs to the process composition root. */
|
|
39
|
+
readonly tls?: HttpHostTlsOptions;
|
|
40
|
+
/**
|
|
41
|
+
* `http.trustProxy`: which TCP peers are reverse proxies whose `X-Forwarded-For` / `Forwarded`
|
|
42
|
+
* names the client. Off by default (the peer address is the client address).
|
|
43
|
+
*/
|
|
44
|
+
readonly trustProxy?: TrustProxySetting;
|
|
45
|
+
/** @internal Shared registry for listener groups; `token` is ignored when set. */
|
|
46
|
+
readonly tokenRegistry?: TokenRegistry;
|
|
47
|
+
}
|