@zhin.js/host-http 1.0.10 → 1.0.11

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.
@@ -81,6 +81,16 @@ export type RuntimeConsoleRpcContext = {
81
81
  * Returns whether a process restart is required (always true without ConfigFeature).
82
82
  */
83
83
  setConfigKey?(pluginName: string, data: unknown): Promise<{ restartRequired: boolean }>;
84
+ /** Atomic, revision-checked runtime Workroom Catalog read/write. */
85
+ readWorkroomCatalog?(): Promise<Readonly<{
86
+ agents: Readonly<Record<string, unknown>>;
87
+ workrooms: Readonly<Record<string, unknown>>;
88
+ revision: string;
89
+ }>>;
90
+ setWorkroomCatalog?(
91
+ workrooms: unknown,
92
+ expectedRevision: string,
93
+ ): Promise<Readonly<{ revision: string; restartRequired: boolean }>>;
84
94
  /** Project file manager (allowlisted paths under project root). */
85
95
  listProjectFiles?(): Promise<readonly unknown[]>;
86
96
  readProjectFile?(filePath: string): Promise<{ content: string; size: number }>;
@@ -240,6 +250,39 @@ export async function dispatchRuntimeConsoleRpc(
240
250
  }
241
251
  return payloads;
242
252
  }
253
+ case 'workrooms:get': {
254
+ try {
255
+ if (!ctx.readWorkroomCatalog) {
256
+ emit({ requestId, error: 'Workroom Catalog read is not configured' });
257
+ return payloads;
258
+ }
259
+ emit({ requestId, data: await ctx.readWorkroomCatalog() });
260
+ } catch (error) {
261
+ emit({ requestId, error: error instanceof Error ? error.message : String(error) });
262
+ }
263
+ return payloads;
264
+ }
265
+ case 'workrooms:set': {
266
+ try {
267
+ if (!ctx.setWorkroomCatalog) {
268
+ emit({ requestId, error: 'Workroom Catalog write is not configured' });
269
+ return payloads;
270
+ }
271
+ const expectedRevision = typeof message.expectedRevision === 'string'
272
+ ? message.expectedRevision
273
+ : '';
274
+ if (!/^[a-f0-9]{64}$/u.test(expectedRevision)) {
275
+ emit({ requestId, error: 'expectedRevision is required' });
276
+ return payloads;
277
+ }
278
+ const result = await ctx.setWorkroomCatalog(message.data, expectedRevision);
279
+ ctx.publishEvent?.('workrooms:updated', { revision: result.revision });
280
+ emit({ requestId, data: { success: true, ...result } });
281
+ } catch (error) {
282
+ emit({ requestId, error: error instanceof Error ? error.message : String(error) });
283
+ }
284
+ return payloads;
285
+ }
243
286
  case 'config:save-yaml': {
244
287
  try {
245
288
  const yaml = message.yaml;
package/src/http-host.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  TokenRegistry,
18
18
  extractBearerToken,
19
19
  isDemoWebSocketPath,
20
+ type AuthenticatedTokenPrincipal,
20
21
  type AuthScope,
21
22
  type ScopedTokenConfig,
22
23
  type TokenRegistryConfig,
@@ -73,6 +74,7 @@ export type HttpHandler = (
73
74
  response: ServerResponse,
74
75
  url: URL,
75
76
  authScope: AuthScope,
77
+ authenticatedPrincipal?: AuthenticatedTokenPrincipal,
76
78
  ) => void | Promise<void>;
77
79
 
78
80
  export interface HttpRouteRegistration {
@@ -323,7 +325,7 @@ export function createHttpHost(options: HttpHostOptions = {}): ProcessHttpHost {
323
325
  return;
324
326
  }
325
327
  if (!release) {
326
- await route.handler(request, response, url, auth.scope);
328
+ await route.handler(request, response, url, auth.scope, auth.principal);
327
329
  return;
328
330
  }
329
331
  let released = false;
@@ -336,7 +338,7 @@ export function createHttpHost(options: HttpHostOptions = {}): ProcessHttpHost {
336
338
  };
337
339
  response.once('finish', releaseResponse);
338
340
  response.once('close', releaseResponse);
339
- await route.handler(request, response, url, auth.scope);
341
+ await route.handler(request, response, url, auth.scope, auth.principal);
340
342
  if (response.writableFinished || response.destroyed) releaseResponse();
341
343
  } catch (err) {
342
344
  // 统一把请求体错误(400/413)映射回对应状态码,
@@ -363,7 +365,7 @@ export function createHttpHost(options: HttpHostOptions = {}): ProcessHttpHost {
363
365
  request: IncomingMessage,
364
366
  url: URL,
365
367
  pathname: string,
366
- ): { ok: true; scope: AuthScope } | { ok: false } {
368
+ ): { ok: true; scope: AuthScope; principal?: AuthenticatedTokenPrincipal } | { ok: false } {
367
369
  if (!requiresHttpAuth(pathname, apiBase, authExempt)) {
368
370
  return { ok: true, scope: 'full' };
369
371
  }
@@ -380,7 +382,8 @@ export function createHttpHost(options: HttpHostOptions = {}): ProcessHttpHost {
380
382
  const method = (request.method ?? 'GET').toUpperCase();
381
383
  if (!isDemoHttpAllowed(method, pathname, apiBase)) return { ok: false };
382
384
  }
383
- return { ok: true, scope };
385
+ const principal = tokenRegistry.resolvePrincipal(token);
386
+ return { ok: true, scope, ...(principal ? { principal } : {}) };
384
387
  }
385
388
 
386
389
  function listListedRoutes(): readonly ListedRoute[] {
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  TokenRegistry,
16
16
  extractBearerToken,
17
17
  isDemoWebSocketPath,
18
+ type AuthenticatedTokenPrincipal,
18
19
  type AuthScope,
19
20
  type ScopedTokenConfig,
20
21
  type TokenRegistryConfig,
@@ -70,5 +71,7 @@ export {
70
71
  dispatchExtendedConsoleRpc,
71
72
  type ConsoleRpcExtendedCtx,
72
73
  type ConsoleScheduleEngine,
74
+ type ConsoleWorkroomProfileControlPort,
75
+ type ConsoleWorkroomKnowledgeControlPort,
73
76
  type ExtendedRpcResult,
74
77
  } from './console-rpc-extended.js';
@@ -6,27 +6,55 @@ export type AuthScope = 'full' | 'demo';
6
6
  export type ScopedTokenConfig = {
7
7
  readonly token: string;
8
8
  readonly scope: AuthScope;
9
+ /** Root-configured subject binding; never accepted from an HTTP payload. */
10
+ readonly principalId?: string;
9
11
  };
10
12
 
13
+ export interface AuthenticatedTokenPrincipal {
14
+ readonly principalId: string;
15
+ readonly scope: AuthScope;
16
+ }
17
+
11
18
  export type TokenRegistryConfig = {
12
19
  readonly primaryToken?: string;
13
20
  readonly scopedTokens?: readonly ScopedTokenConfig[];
14
21
  };
15
22
 
16
23
  export class TokenRegistry {
17
- readonly #entries = new Map<string, AuthScope>();
24
+ readonly #entries = new Map<string, Readonly<{ scope: AuthScope; principalId?: string }>>();
18
25
 
19
26
  constructor(config: TokenRegistryConfig = {}) {
20
- if (config.primaryToken) this.#entries.set(config.primaryToken, 'full');
21
- for (const { token, scope } of config.scopedTokens ?? []) {
22
- if (token) this.#entries.set(token, scope);
27
+ if (config.primaryToken) this.#entries.set(config.primaryToken, Object.freeze({ scope: 'full' }));
28
+ for (const { token, scope, principalId } of config.scopedTokens ?? []) {
29
+ if (!token) continue;
30
+ if (principalId !== undefined && (!principalId.trim() || principalId !== principalId.trim())) {
31
+ throw new Error('HTTP token principalId is invalid');
32
+ }
33
+ const candidate = Object.freeze({ scope, ...(principalId === undefined ? {} : { principalId }) });
34
+ const existing = this.#entries.get(token);
35
+ if (existing && (existing.scope !== candidate.scope || existing.principalId !== candidate.principalId)) {
36
+ throw new Error('HTTP token has conflicting authority bindings');
37
+ }
38
+ this.#entries.set(token, candidate);
23
39
  }
24
40
  }
25
41
 
26
42
  resolve(token: string): AuthScope | null {
27
43
  if (!token) return null;
28
- for (const [known, scope] of this.#entries) {
29
- if (timingSafeEqualString(known, token)) return scope;
44
+ for (const [known, binding] of this.#entries) {
45
+ if (timingSafeEqualString(known, token)) return binding.scope;
46
+ }
47
+ return null;
48
+ }
49
+
50
+ resolvePrincipal(token: string): AuthenticatedTokenPrincipal | null {
51
+ if (!token) return null;
52
+ for (const [known, binding] of this.#entries) {
53
+ if (timingSafeEqualString(known, token)) {
54
+ return binding.principalId
55
+ ? Object.freeze({ principalId: binding.principalId, scope: binding.scope })
56
+ : null;
57
+ }
30
58
  }
31
59
  return null;
32
60
  }
@@ -36,8 +64,8 @@ export class TokenRegistry {
36
64
  }
37
65
 
38
66
  primaryTokenPrefixForLog(): string {
39
- for (const [tok, scope] of this.#entries) {
40
- if (scope === 'full') return tok.slice(0, 6);
67
+ for (const [tok, binding] of this.#entries) {
68
+ if (binding.scope === 'full') return tok.slice(0, 6);
41
69
  }
42
70
  const first = this.#entries.keys().next().value;
43
71
  return first ? first.slice(0, 6) : '';