@zhin.js/host-http 1.1.11 → 1.1.12

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,3 +1,9 @@
1
+ interface LoginEndpointIdentity {
2
+ readonly canonicalId: string;
3
+ readonly instanceId: string;
4
+ readonly epoch: number;
5
+ readonly incarnationId: string;
6
+ }
1
7
  export interface ConsoleRpcExtendedCtx {
2
8
  /** full scope 才允许写操作;demo scope 只放行只读 RPC。 */
3
9
  fullScope: boolean;
@@ -20,13 +26,14 @@ export interface ConsoleRpcExtendedCtx {
20
26
  id: string;
21
27
  adapter: string;
22
28
  endpointKey: string;
29
+ endpointIdentity?: LoginEndpointIdentity;
23
30
  type: string;
24
31
  payload: Record<string, unknown>;
25
32
  createdAt: number;
26
33
  expiresAt?: number;
27
34
  }[];
28
- submit(id: string, value: string | Record<string, unknown>): boolean;
29
- cancel(id: string, reason?: string): boolean;
35
+ submit(id: string, value: string | Record<string, unknown>, endpointIdentity?: LoginEndpointIdentity): boolean;
36
+ cancel(id: string, reason?: string, endpointIdentity?: LoginEndpointIdentity): boolean;
30
37
  };
31
38
  /** Authenticated Root adapter; mutation DTOs deliberately contain no principal/decision fields. */
32
39
  workroomProfileControl?: ConsoleWorkroomProfileControlPort;
@@ -137,6 +144,33 @@ export interface InboxModel {
137
144
  where(query: Record<string, unknown>): Promise<unknown> | unknown;
138
145
  };
139
146
  }
147
+ /** HTTP's narrow structural seam; implementations keep platform objects and credentials private. */
148
+ export interface EndpointGroupCommand {
149
+ readonly groupId: string;
150
+ readonly signal?: AbortSignal;
151
+ }
152
+ export interface EndpointUserCommand {
153
+ readonly userId: string;
154
+ readonly signal?: AbortSignal;
155
+ }
156
+ export interface EndpointMemberCommand extends EndpointGroupCommand, EndpointUserCommand {
157
+ }
158
+ export interface EndpointMuteCommand extends EndpointMemberCommand {
159
+ readonly durationSeconds: number;
160
+ }
161
+ export interface EndpointAdminCommand extends EndpointMemberCommand {
162
+ readonly enabled: boolean;
163
+ }
164
+ export interface EndpointApproveCommand {
165
+ readonly requestId: string;
166
+ readonly remark?: string;
167
+ readonly signal?: AbortSignal;
168
+ }
169
+ export interface EndpointRejectCommand {
170
+ readonly requestId: string;
171
+ readonly reason?: string;
172
+ readonly signal?: AbortSignal;
173
+ }
140
174
  export interface EndpointManagementPort {
141
175
  listFriends?(): Promise<readonly {
142
176
  user_id: number | string;
@@ -156,7 +190,7 @@ export interface EndpointManagementPort {
156
190
  name?: string;
157
191
  };
158
192
  }[]>;
159
- listGroupMembers?(groupId: string): Promise<readonly unknown[]>;
193
+ listGroupMembers?(request: EndpointGroupCommand): Promise<readonly unknown[]>;
160
194
  listRequests?(): Promise<readonly {
161
195
  platform_request_id: string;
162
196
  type: string;
@@ -168,10 +202,11 @@ export interface EndpointManagementPort {
168
202
  comment?: string | null;
169
203
  created_at: number;
170
204
  }[]>;
171
- approveRequest?(requestId: string, remark?: string): Promise<void>;
172
- rejectRequest?(requestId: string, reason?: string): Promise<void>;
173
- kickGroupMember?(groupId: string, userId: string): Promise<void>;
174
- muteGroupMember?(groupId: string, userId: string, durationSeconds: number): Promise<void>;
175
- setGroupAdmin?(groupId: string, userId: string, enabled: boolean): Promise<void>;
176
- deleteFriend?(userId: string): Promise<void>;
205
+ approveRequest?(request: EndpointApproveCommand): Promise<void>;
206
+ rejectRequest?(request: EndpointRejectCommand): Promise<void>;
207
+ kickGroupMember?(request: EndpointMemberCommand): Promise<void>;
208
+ muteGroupMember?(request: EndpointMuteCommand): Promise<void>;
209
+ setGroupAdmin?(request: EndpointAdminCommand): Promise<void>;
210
+ deleteFriend?(request: EndpointUserCommand): Promise<void>;
177
211
  }
212
+ export {};
@@ -57,22 +57,16 @@ export async function dispatchExtendedConsoleRpc(type, data, ctx) {
57
57
  case 'endpoint.group_kick':
58
58
  return groupWriteOp(d, ctx, {
59
59
  method: 'kickGroupMember',
60
- buildArgs: (gid, uid) => [gid, uid],
61
- requireUser: true,
62
60
  unsupported: '踢出群成员',
63
61
  });
64
62
  case 'endpoint.group_mute':
65
63
  return groupWriteOp(d, ctx, {
66
64
  method: 'muteGroupMember',
67
- buildArgs: (gid, uid, extra) => [gid, uid, extra.duration ?? 600],
68
- requireUser: true,
69
65
  unsupported: '禁言群成员',
70
66
  });
71
67
  case 'endpoint.group_admin':
72
68
  return groupWriteOp(d, ctx, {
73
69
  method: 'setGroupAdmin',
74
- buildArgs: (gid, uid, extra) => [gid, uid, extra.enable !== false],
75
- requireUser: true,
76
70
  unsupported: '设置群管理员',
77
71
  });
78
72
  case 'endpoint.delete_friend':
@@ -5,11 +5,6 @@ export declare function listChannels(d: Record<string, unknown>, ctx: ConsoleRpc
5
5
  export declare function listGroupMembers(d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx): Promise<ExtendedRpcResult>;
6
6
  interface GroupWriteSpec {
7
7
  method: 'kickGroupMember' | 'muteGroupMember' | 'setGroupAdmin';
8
- buildArgs(groupId: string, userId: string, extra: {
9
- duration?: number;
10
- enable?: boolean;
11
- }): unknown[];
12
- requireUser: boolean;
13
8
  /** 中文操作名,用于"该平台不支持 xxx"。 */
14
9
  unsupported: string;
15
10
  }
@@ -47,7 +47,7 @@ export async function listGroupMembers(d, ctx) {
47
47
  if (!method) {
48
48
  return { error: `当前适配器(${adapter})不支持群成员列表` };
49
49
  }
50
- const members = [...await method(groupId)];
50
+ const members = [...await method.call(management, { groupId })];
51
51
  return { data: { members, count: members.length } };
52
52
  });
53
53
  }
@@ -59,7 +59,7 @@ export async function groupWriteOp(d, ctx, spec) {
59
59
  if (!adapter || !endpointKey || !groupId) {
60
60
  return { error: 'adapter, endpointKey, and groupId are required' };
61
61
  }
62
- if (spec.requireUser && !userId) {
62
+ if (!userId) {
63
63
  return { error: 'userId is required' };
64
64
  }
65
65
  return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
@@ -69,13 +69,19 @@ export async function groupWriteOp(d, ctx, spec) {
69
69
  }
70
70
  const durationRaw = d.duration;
71
71
  const enableRaw = d.enable;
72
- const args = spec.buildArgs(groupId, userId, {
73
- duration: typeof durationRaw === 'number' && Number.isFinite(durationRaw)
74
- ? durationRaw
75
- : undefined,
76
- enable: typeof enableRaw === 'boolean' ? enableRaw : undefined,
77
- });
78
- await method.call(management, ...args);
72
+ // The discriminant selects one typed semantic request; no argument arrays or casts.
73
+ switch (spec.method) {
74
+ case 'kickGroupMember':
75
+ await management.kickGroupMember({ groupId, userId });
76
+ break;
77
+ case 'muteGroupMember':
78
+ await management.muteGroupMember({ groupId, userId,
79
+ durationSeconds: typeof durationRaw === 'number' && Number.isFinite(durationRaw) ? durationRaw : 600 });
80
+ break;
81
+ case 'setGroupAdmin':
82
+ await management.setGroupAdmin({ groupId, userId, enabled: enableRaw !== false });
83
+ break;
84
+ }
79
85
  return { data: { success: true } };
80
86
  });
81
87
  }
@@ -91,7 +97,7 @@ export async function deleteFriend(d, ctx) {
91
97
  if (!method) {
92
98
  return { error: '当前适配器暂不支持删除好友' };
93
99
  }
94
- await method(userId);
100
+ await method.call(management, { userId });
95
101
  return { data: { success: true } };
96
102
  });
97
103
  }
@@ -322,7 +322,10 @@ export async function actOnRequest(type, d, ctx) {
322
322
  const extra = approve
323
323
  ? strField(d, 'remark')
324
324
  : strField(d, 'reason');
325
- await method.call(management, requestId, extra || undefined);
325
+ if (approve)
326
+ await management.approveRequest({ requestId, remark: extra || undefined });
327
+ else
328
+ await management.rejectRequest({ requestId, reason: extra || undefined });
326
329
  const model = getInboxModel(ctx, TABLE_REQUEST);
327
330
  if (model && typeof model.update === 'function') {
328
331
  await model.update({ resolved: 1, resolved_at: Date.now() }).where({
@@ -7,6 +7,7 @@ export function listLoginPending(ctx) {
7
7
  id: task.id,
8
8
  adapter: task.adapter,
9
9
  endpointKey: task.endpointKey,
10
+ ...(task.endpointIdentity ? { endpointIdentity: task.endpointIdentity } : {}),
10
11
  type: task.type,
11
12
  payload: task.payload,
12
13
  createdAt: task.createdAt,
@@ -25,7 +26,10 @@ export function submitLogin(d, ctx) {
25
26
  const value = typeof raw === 'string' || (raw && typeof raw === 'object')
26
27
  ? raw
27
28
  : String(raw ?? '');
28
- const ok = assist.submit(id, value);
29
+ const identity = readEndpointIdentity(d.endpointIdentity);
30
+ if (identity === false)
31
+ return { error: 'Invalid login Endpoint identity' };
32
+ const ok = identity ? assist.submit(id, value, identity) : assist.submit(id, value);
29
33
  if (!ok)
30
34
  return { error: `login task not found: ${id}` };
31
35
  return { data: { success: true } };
@@ -38,9 +42,23 @@ export function cancelLogin(d, ctx) {
38
42
  if (!id)
39
43
  return { error: 'taskId is required' };
40
44
  const reason = strField(d, 'reason') || 'cancelled';
41
- const ok = assist.cancel(id, reason);
45
+ const identity = readEndpointIdentity(d.endpointIdentity);
46
+ if (identity === false)
47
+ return { error: 'Invalid login Endpoint identity' };
48
+ const ok = identity ? assist.cancel(id, reason, identity) : assist.cancel(id, reason);
42
49
  if (!ok)
43
50
  return { error: `login task not found: ${id}` };
44
51
  return { data: { success: true } };
45
52
  }
46
53
  // ---------------------------------------------------------------- 社交读取
54
+ function readEndpointIdentity(value) {
55
+ if (value === undefined)
56
+ return undefined;
57
+ if (!value || typeof value !== 'object' || Array.isArray(value))
58
+ return false;
59
+ const identity = value;
60
+ if (Object.keys(identity).length !== 4 || typeof identity.canonicalId !== 'string' || !identity.canonicalId
61
+ || typeof identity.instanceId !== 'string' || !identity.instanceId || typeof identity.incarnationId !== 'string' || !identity.incarnationId || !Number.isSafeInteger(identity.epoch) || Number(identity.epoch) < 1)
62
+ return false;
63
+ return { canonicalId: identity.canonicalId, instanceId: identity.instanceId, epoch: Number(identity.epoch), incarnationId: identity.incarnationId };
64
+ }
@@ -838,6 +838,12 @@ function errorReply(payloads, requestId, error) {
838
838
  export function isDemoHttpAllowed(method, pathname, apiBase) {
839
839
  const normalizedBase = apiBase.replace(/\/$/u, '') || '/api';
840
840
  const m = method.toUpperCase();
841
+ // These handlers require an authenticated principal and verify its bound personal owner.
842
+ if ((m === 'GET' || m === 'PUT') && (pathname === `${normalizedBase}/agent/personal-models`
843
+ || pathname === `${normalizedBase}/agent/personal-session`))
844
+ return true;
845
+ if (m === 'GET' && pathname === `${normalizedBase}/agent/personal-sessions`)
846
+ return true;
841
847
  if (m === 'GET' && pathname === `${normalizedBase}/events`)
842
848
  return true;
843
849
  if (m === 'GET' && pathname === `${normalizedBase}/events/history`)
package/lib/http-host.js CHANGED
@@ -112,26 +112,30 @@ export function createHttpHost(options = {}) {
112
112
  try {
113
113
  wss.handleUpgrade(request, socket, head, (ws) => {
114
114
  let settled = false;
115
- const retireSubscriptions = [];
115
+ const stopSubscriptions = [];
116
116
  const releaseAdmissions = () => {
117
117
  if (settled)
118
118
  return;
119
119
  settled = true;
120
- for (const dispose of retireSubscriptions)
121
- dispose();
120
+ for (const unsubscribe of stopSubscriptions)
121
+ unsubscribe();
122
122
  for (const item of admitted)
123
123
  item.release?.();
124
124
  };
125
125
  ws.once('close', releaseAdmissions);
126
126
  ws.once('error', releaseAdmissions);
127
- for (const item of admitted) {
128
- if (item.entry.admission) {
129
- retireSubscriptions.push(item.entry.admission.onDeactivate(() => {
130
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
131
- ws.close(1012, 'Generation retired');
132
- }
133
- }));
134
- }
127
+ // Replacement closes new upgrades, not accepted plugin work. Explicit Root stop
128
+ // still closes its sockets before waiting for their admission leases to drain.
129
+ const stops = new Set(admitted.map(item => item.entry.admission?.stopSignal).filter(signal => signal !== undefined));
130
+ for (const signal of stops) {
131
+ const stop = () => {
132
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)
133
+ ws.close(1012, 'Root stopped');
134
+ };
135
+ signal.addEventListener('abort', stop, { once: true });
136
+ stopSubscriptions.push(() => signal.removeEventListener('abort', stop));
137
+ if (signal.aborted)
138
+ stop();
135
139
  }
136
140
  const connection = Object.freeze({
137
141
  socket: ws,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/host-http",
3
- "version": "1.1.11",
3
+ "version": "1.1.12",
4
4
  "description": "Minimal HTTP and WebSocket host for Plugin Runtime (no Koa)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -21,7 +21,7 @@
21
21
  "@zhin.js/host-http-contract": "1.1.1",
22
22
  "@zhin.js/logger": "1.1.2",
23
23
  "@zhin.js/schedule": "1.1.3",
24
- "@zhin.js/plugin-runtime": "1.1.17"
24
+ "@zhin.js/plugin-runtime": "1.1.18"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.1.2",
@@ -1,3 +1,10 @@
1
+ interface LoginEndpointIdentity {
2
+ readonly canonicalId: string;
3
+ readonly instanceId: string;
4
+ readonly epoch: number;
5
+ readonly incarnationId: string;
6
+ }
7
+
1
8
  export interface ConsoleRpcExtendedCtx {
2
9
  /** full scope 才允许写操作;demo scope 只放行只读 RPC。 */
3
10
  fullScope: boolean;
@@ -20,13 +27,14 @@ export interface ConsoleRpcExtendedCtx {
20
27
  id: string;
21
28
  adapter: string;
22
29
  endpointKey: string;
30
+ endpointIdentity?: LoginEndpointIdentity;
23
31
  type: string;
24
32
  payload: Record<string, unknown>;
25
33
  createdAt: number;
26
34
  expiresAt?: number;
27
35
  }[];
28
- submit(id: string, value: string | Record<string, unknown>): boolean;
29
- cancel(id: string, reason?: string): boolean;
36
+ submit(id: string, value: string | Record<string, unknown>, endpointIdentity?: LoginEndpointIdentity): boolean;
37
+ cancel(id: string, reason?: string, endpointIdentity?: LoginEndpointIdentity): boolean;
30
38
  };
31
39
  /** Authenticated Root adapter; mutation DTOs deliberately contain no principal/decision fields. */
32
40
  workroomProfileControl?: ConsoleWorkroomProfileControlPort;
@@ -126,6 +134,15 @@ export interface InboxModel {
126
134
  };
127
135
  }
128
136
 
137
+ /** HTTP's narrow structural seam; implementations keep platform objects and credentials private. */
138
+ export interface EndpointGroupCommand { readonly groupId: string; readonly signal?: AbortSignal }
139
+ export interface EndpointUserCommand { readonly userId: string; readonly signal?: AbortSignal }
140
+ export interface EndpointMemberCommand extends EndpointGroupCommand, EndpointUserCommand {}
141
+ export interface EndpointMuteCommand extends EndpointMemberCommand { readonly durationSeconds: number }
142
+ export interface EndpointAdminCommand extends EndpointMemberCommand { readonly enabled: boolean }
143
+ export interface EndpointApproveCommand { readonly requestId: string; readonly remark?: string; readonly signal?: AbortSignal }
144
+ export interface EndpointRejectCommand { readonly requestId: string; readonly reason?: string; readonly signal?: AbortSignal }
145
+
129
146
  export interface EndpointManagementPort {
130
147
  listFriends?(): Promise<readonly { user_id: number | string; nickname: string; remark: string }[]>;
131
148
  listGroups?(): Promise<readonly { group_id: number | string; name: string }[]>;
@@ -134,7 +151,7 @@ export interface EndpointManagementPort {
134
151
  name?: string;
135
152
  parent?: { type: string; id: string; name?: string };
136
153
  }[]>;
137
- listGroupMembers?(groupId: string): Promise<readonly unknown[]>;
154
+ listGroupMembers?(request: EndpointGroupCommand): Promise<readonly unknown[]>;
138
155
  listRequests?(): Promise<readonly {
139
156
  platform_request_id: string;
140
157
  type: string;
@@ -146,10 +163,10 @@ export interface EndpointManagementPort {
146
163
  comment?: string | null;
147
164
  created_at: number;
148
165
  }[]>;
149
- approveRequest?(requestId: string, remark?: string): Promise<void>;
150
- rejectRequest?(requestId: string, reason?: string): Promise<void>;
151
- kickGroupMember?(groupId: string, userId: string): Promise<void>;
152
- muteGroupMember?(groupId: string, userId: string, durationSeconds: number): Promise<void>;
153
- setGroupAdmin?(groupId: string, userId: string, enabled: boolean): Promise<void>;
154
- deleteFriend?(userId: string): Promise<void>;
166
+ approveRequest?(request: EndpointApproveCommand): Promise<void>;
167
+ rejectRequest?(request: EndpointRejectCommand): Promise<void>;
168
+ kickGroupMember?(request: EndpointMemberCommand): Promise<void>;
169
+ muteGroupMember?(request: EndpointMuteCommand): Promise<void>;
170
+ setGroupAdmin?(request: EndpointAdminCommand): Promise<void>;
171
+ deleteFriend?(request: EndpointUserCommand): Promise<void>;
155
172
  }
@@ -77,22 +77,16 @@ export async function dispatchExtendedConsoleRpc(
77
77
  case 'endpoint.group_kick':
78
78
  return groupWriteOp(d, ctx, {
79
79
  method: 'kickGroupMember',
80
- buildArgs: (gid, uid) => [gid, uid],
81
- requireUser: true,
82
80
  unsupported: '踢出群成员',
83
81
  });
84
82
  case 'endpoint.group_mute':
85
83
  return groupWriteOp(d, ctx, {
86
84
  method: 'muteGroupMember',
87
- buildArgs: (gid, uid, extra) => [gid, uid, extra.duration ?? 600],
88
- requireUser: true,
89
85
  unsupported: '禁言群成员',
90
86
  });
91
87
  case 'endpoint.group_admin':
92
88
  return groupWriteOp(d, ctx, {
93
89
  method: 'setGroupAdmin',
94
- buildArgs: (gid, uid, extra) => [gid, uid, extra.enable !== false],
95
- requireUser: true,
96
90
  unsupported: '设置群管理员',
97
91
  });
98
92
  case 'endpoint.delete_friend':
@@ -61,7 +61,7 @@ export async function listGroupMembers(
61
61
  if (!method) {
62
62
  return { error: `当前适配器(${adapter})不支持群成员列表` };
63
63
  }
64
- const members = [...await method(groupId)];
64
+ const members = [...await method.call(management, { groupId })];
65
65
  return { data: { members, count: members.length } };
66
66
  });
67
67
  }
@@ -70,8 +70,6 @@ export async function listGroupMembers(
70
70
 
71
71
  interface GroupWriteSpec {
72
72
  method: 'kickGroupMember' | 'muteGroupMember' | 'setGroupAdmin';
73
- buildArgs(groupId: string, userId: string, extra: { duration?: number; enable?: boolean }): unknown[];
74
- requireUser: boolean;
75
73
  /** 中文操作名,用于"该平台不支持 xxx"。 */
76
74
  unsupported: string;
77
75
  }
@@ -88,7 +86,7 @@ export async function groupWriteOp(
88
86
  if (!adapter || !endpointKey || !groupId) {
89
87
  return { error: 'adapter, endpointKey, and groupId are required' };
90
88
  }
91
- if (spec.requireUser && !userId) {
89
+ if (!userId) {
92
90
  return { error: 'userId is required' };
93
91
  }
94
92
  return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
@@ -98,13 +96,19 @@ export async function groupWriteOp(
98
96
  }
99
97
  const durationRaw = d.duration;
100
98
  const enableRaw = d.enable;
101
- const args = spec.buildArgs(groupId, userId, {
102
- duration: typeof durationRaw === 'number' && Number.isFinite(durationRaw)
103
- ? durationRaw
104
- : undefined,
105
- enable: typeof enableRaw === 'boolean' ? enableRaw : undefined,
106
- });
107
- await (method as (...args: unknown[]) => Promise<void>).call(management, ...args);
99
+ // The discriminant selects one typed semantic request; no argument arrays or casts.
100
+ switch (spec.method) {
101
+ case 'kickGroupMember':
102
+ await management.kickGroupMember!({ groupId, userId });
103
+ break;
104
+ case 'muteGroupMember':
105
+ await management.muteGroupMember!({ groupId, userId,
106
+ durationSeconds: typeof durationRaw === 'number' && Number.isFinite(durationRaw) ? durationRaw : 600 });
107
+ break;
108
+ case 'setGroupAdmin':
109
+ await management.setGroupAdmin!({ groupId, userId, enabled: enableRaw !== false });
110
+ break;
111
+ }
108
112
  return { data: { success: true } };
109
113
  });
110
114
  }
@@ -124,7 +128,7 @@ export async function deleteFriend(
124
128
  if (!method) {
125
129
  return { error: '当前适配器暂不支持删除好友' };
126
130
  }
127
- await method(userId);
131
+ await method.call(management, { userId });
128
132
  return { data: { success: true } };
129
133
  });
130
134
  }
@@ -364,7 +364,8 @@ export async function actOnRequest(
364
364
  const extra = approve
365
365
  ? strField(d, 'remark')
366
366
  : strField(d, 'reason');
367
- await method.call(management, requestId, extra || undefined);
367
+ if (approve) await management.approveRequest!({ requestId, remark: extra || undefined });
368
+ else await management.rejectRequest!({ requestId, reason: extra || undefined });
368
369
  const model = getInboxModel(ctx, TABLE_REQUEST);
369
370
  if (model && typeof model.update === 'function') {
370
371
  await model.update({resolved: 1, resolved_at: Date.now()}).where({
@@ -8,6 +8,7 @@ export function listLoginPending(ctx: ConsoleRpcExtendedCtx): ExtendedRpcResult
8
8
  id: task.id,
9
9
  adapter: task.adapter,
10
10
  endpointKey: task.endpointKey,
11
+ ...(task.endpointIdentity ? { endpointIdentity: task.endpointIdentity } : {}),
11
12
  type: task.type,
12
13
  payload: task.payload,
13
14
  createdAt: task.createdAt,
@@ -28,7 +29,9 @@ export function submitLogin(
28
29
  const value = typeof raw === 'string' || (raw && typeof raw === 'object')
29
30
  ? raw as string | Record<string, unknown>
30
31
  : String(raw ?? '');
31
- const ok = assist.submit(id, value);
32
+ const identity = readEndpointIdentity(d.endpointIdentity);
33
+ if (identity === false) return { error: 'Invalid login Endpoint identity' };
34
+ const ok = identity ? assist.submit(id, value, identity) : assist.submit(id, value);
32
35
  if (!ok) return { error: `login task not found: ${id}` };
33
36
  return { data: { success: true } };
34
37
  }
@@ -42,9 +45,20 @@ export function cancelLogin(
42
45
  const id = strField(d, 'taskId');
43
46
  if (!id) return { error: 'taskId is required' };
44
47
  const reason = strField(d, 'reason') || 'cancelled';
45
- const ok = assist.cancel(id, reason);
48
+ const identity = readEndpointIdentity(d.endpointIdentity);
49
+ if (identity === false) return { error: 'Invalid login Endpoint identity' };
50
+ const ok = identity ? assist.cancel(id, reason, identity) : assist.cancel(id, reason);
46
51
  if (!ok) return { error: `login task not found: ${id}` };
47
52
  return { data: { success: true } };
48
53
  }
49
54
 
50
55
  // ---------------------------------------------------------------- 社交读取
56
+
57
+ function readEndpointIdentity(value: unknown): { readonly canonicalId: string; readonly instanceId: string; readonly epoch: number; readonly incarnationId: string } | false | undefined {
58
+ if (value === undefined) return undefined;
59
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
60
+ const identity = value as Record<string, unknown>;
61
+ if (Object.keys(identity).length !== 4 || typeof identity.canonicalId !== 'string' || !identity.canonicalId
62
+ || typeof identity.instanceId !== 'string' || !identity.instanceId || typeof identity.incarnationId !== 'string' || !identity.incarnationId || !Number.isSafeInteger(identity.epoch) || Number(identity.epoch) < 1) return false;
63
+ return { canonicalId: identity.canonicalId, instanceId: identity.instanceId, epoch: Number(identity.epoch), incarnationId: identity.incarnationId };
64
+ }
@@ -1056,6 +1056,10 @@ function errorReply(
1056
1056
  export function isDemoHttpAllowed(method: string, pathname: string, apiBase: string): boolean {
1057
1057
  const normalizedBase = apiBase.replace(/\/$/u, '') || '/api';
1058
1058
  const m = method.toUpperCase();
1059
+ // These handlers require an authenticated principal and verify its bound personal owner.
1060
+ if ((m === 'GET' || m === 'PUT') && (pathname === `${normalizedBase}/agent/personal-models`
1061
+ || pathname === `${normalizedBase}/agent/personal-session`)) return true;
1062
+ if (m === 'GET' && pathname === `${normalizedBase}/agent/personal-sessions`) return true;
1059
1063
  if (m === 'GET' && pathname === `${normalizedBase}/events`) return true;
1060
1064
  if (m === 'GET' && pathname === `${normalizedBase}/events/history`) return true;
1061
1065
  if (m === 'GET' && DEMO_INTROSPECTION_PATHS.has(pathname.slice(normalizedBase.length))) return true;
package/src/http-host.ts CHANGED
@@ -256,23 +256,25 @@ export function createHttpHost(options: HttpHostOptions = {}): ProcessHttpHost {
256
256
  try {
257
257
  wss.handleUpgrade(request, socket, head, (ws) => {
258
258
  let settled = false;
259
- const retireSubscriptions: Array<() => void> = [];
259
+ const stopSubscriptions: Array<() => void> = [];
260
260
  const releaseAdmissions = () => {
261
261
  if (settled) return;
262
262
  settled = true;
263
- for (const dispose of retireSubscriptions) dispose();
263
+ for (const unsubscribe of stopSubscriptions) unsubscribe();
264
264
  for (const item of admitted) item.release?.();
265
265
  };
266
266
  ws.once('close', releaseAdmissions);
267
267
  ws.once('error', releaseAdmissions);
268
- for (const item of admitted) {
269
- if (item.entry.admission) {
270
- retireSubscriptions.push(item.entry.admission.onDeactivate(() => {
271
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
272
- ws.close(1012, 'Generation retired');
273
- }
274
- }));
275
- }
268
+ // Replacement closes new upgrades, not accepted plugin work. Explicit Root stop
269
+ // still closes its sockets before waiting for their admission leases to drain.
270
+ const stops = new Set(admitted.map(item => item.entry.admission?.stopSignal).filter(signal => signal !== undefined));
271
+ for (const signal of stops) {
272
+ const stop = () => {
273
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) ws.close(1012, 'Root stopped');
274
+ };
275
+ signal.addEventListener('abort', stop, { once: true });
276
+ stopSubscriptions.push(() => signal.removeEventListener('abort', stop));
277
+ if (signal.aborted) stop();
276
278
  }
277
279
  const connection = Object.freeze({
278
280
  socket: ws,