@agentunion/fastaun 0.4.7 → 0.4.9
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/CHANGELOG.md +41 -0
- package/_packed_docs/CHANGELOG.md +41 -0
- package/_packed_docs/INDEX.md +61 -31
- package/_packed_docs/KITE_DOCS_GUIDE.md +18 -13
- package/_packed_docs/design/AUNClient/346/213/206/345/210/206/351/207/215/346/236/204/346/211/247/350/241/214/346/226/271/346/241/210.md +859 -0
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +5 -4
- package/_packed_docs/sdk/INDEX.md +15 -9
- package/_packed_docs/sdk/README.md +3 -2
- package/dist/agent-md.d.ts +1 -1
- package/dist/agent-md.js +12 -5
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +0 -1
- package/dist/aid-store.js +26 -13
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +1 -0
- package/dist/aid.js +8 -3
- package/dist/aid.js.map +1 -1
- package/dist/cert-utils.d.ts +5 -1
- package/dist/cert-utils.js +47 -9
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +50 -0
- package/dist/client/delivery.js +1147 -0
- package/dist/client/delivery.js.map +1 -0
- package/dist/client/group-state.d.ts +31 -0
- package/dist/client/group-state.js +845 -0
- package/dist/client/group-state.js.map +1 -0
- package/dist/client/identity.d.ts +7 -0
- package/dist/client/identity.js +29 -0
- package/dist/client/identity.js.map +1 -0
- package/dist/client/lifecycle.d.ts +11 -0
- package/dist/client/lifecycle.js +292 -0
- package/dist/client/lifecycle.js.map +1 -0
- package/dist/client/peers.d.ts +10 -0
- package/dist/client/peers.js +42 -0
- package/dist/client/peers.js.map +1 -0
- package/dist/client/rpc-pipeline.d.ts +37 -0
- package/dist/client/rpc-pipeline.js +614 -0
- package/dist/client/rpc-pipeline.js.map +1 -0
- package/dist/client/runtime.d.ts +89 -0
- package/dist/client/runtime.js +241 -0
- package/dist/client/runtime.js.map +1 -0
- package/dist/client/v2-e2ee.d.ts +109 -0
- package/dist/client/v2-e2ee.js +1633 -0
- package/dist/client/v2-e2ee.js.map +1 -0
- package/dist/client.d.ts +22 -77
- package/dist/client.js +313 -4114
- package/dist/client.js.map +1 -1
- package/dist/config.js +9 -8
- package/dist/config.js.map +1 -1
- package/dist/discovery.js +56 -6
- package/dist/discovery.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +2 -2
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import { normalizeSlotId, slotIsolationKey } from '../config.js';
|
|
3
|
+
import { ClientSignatureError, ConnectionError, PermissionError, StateError, ValidationError } from '../errors.js';
|
|
4
|
+
import { normalizeGroupId } from '../group-id.js';
|
|
5
|
+
import { ConnectionState, isJsonObject, } from '../types.js';
|
|
6
|
+
const INTERNAL_ONLY_METHODS = new Set([
|
|
7
|
+
'auth.login1',
|
|
8
|
+
'auth.aid_login1',
|
|
9
|
+
'auth.login2',
|
|
10
|
+
'auth.aid_login2',
|
|
11
|
+
'auth.connect',
|
|
12
|
+
'auth.refresh_token',
|
|
13
|
+
'initialize',
|
|
14
|
+
]);
|
|
15
|
+
const REMOVED_E2EE_METHODS = new Set([
|
|
16
|
+
'group.rotate_epoch',
|
|
17
|
+
'group.e2ee.begin_rotation',
|
|
18
|
+
'group.e2ee.commit_rotation',
|
|
19
|
+
'group.e2ee.abort_rotation',
|
|
20
|
+
]);
|
|
21
|
+
const PROTECTED_HEADERS_METHODS = new Set([
|
|
22
|
+
'message.send',
|
|
23
|
+
'group.send',
|
|
24
|
+
'message.thought.put',
|
|
25
|
+
'group.thought.put',
|
|
26
|
+
]);
|
|
27
|
+
const SIGNED_METHODS = new Set([
|
|
28
|
+
'message.send',
|
|
29
|
+
'message.v2.put_peer_pk', 'message.v2.bootstrap',
|
|
30
|
+
'message.v2.group_bootstrap', 'message.v2.pull',
|
|
31
|
+
'message.v2.ack',
|
|
32
|
+
'group.send',
|
|
33
|
+
'group.v2.put_group_pk', 'group.v2.bootstrap',
|
|
34
|
+
'group.v2.send', 'group.v2.pull', 'group.v2.ack',
|
|
35
|
+
'group.v2.propose_state', 'group.v2.confirm_state',
|
|
36
|
+
'group.v2.get_proposal',
|
|
37
|
+
'group.kick', 'group.add_member',
|
|
38
|
+
'group.leave', 'group.remove_member', 'group.update_rules',
|
|
39
|
+
'group.update', 'group.update_announcement',
|
|
40
|
+
'group.update_join_requirements', 'group.set_role',
|
|
41
|
+
'group.transfer_owner', 'group.review_join_request',
|
|
42
|
+
'group.batch_review_join_request',
|
|
43
|
+
'group.request_join', 'group.use_invite_code',
|
|
44
|
+
'group.thought.put',
|
|
45
|
+
'message.thought.put',
|
|
46
|
+
'group.set_settings',
|
|
47
|
+
'group.resources.put', 'group.resources.update',
|
|
48
|
+
'group.resources.delete', 'group.resources.request_add',
|
|
49
|
+
'group.resources.direct_add', 'group.resources.approve_request',
|
|
50
|
+
'group.resources.reject_request',
|
|
51
|
+
'group.commit_state',
|
|
52
|
+
'group.ban', 'group.unban',
|
|
53
|
+
'group.dissolve', 'group.suspend', 'group.resume',
|
|
54
|
+
]);
|
|
55
|
+
/** pull-gate 防并发窗口,与 client.ts PULL_GATE_STALE_MS 保持一致 */
|
|
56
|
+
const PULL_GATE_STALE_MS = 30000;
|
|
57
|
+
const NON_IDEMPOTENT_TIMEOUT_MS = 35_000;
|
|
58
|
+
const NON_IDEMPOTENT_METHODS = new Set([
|
|
59
|
+
'message.send', 'group.send', 'group.create', 'group.invite',
|
|
60
|
+
'group.kick', 'group.remove_member', 'group.leave', 'group.dissolve',
|
|
61
|
+
'group.update_name', 'group.update_avatar', 'group.update_announcement',
|
|
62
|
+
'group.update_settings',
|
|
63
|
+
'storage.upload', 'storage.complete_upload', 'storage.delete',
|
|
64
|
+
'auth.create_aid', 'auth.renew_cert', 'auth.rekey',
|
|
65
|
+
'message.thought.put', 'group.thought.put',
|
|
66
|
+
'group.add_member',
|
|
67
|
+
]);
|
|
68
|
+
export class RpcPipeline {
|
|
69
|
+
runtime;
|
|
70
|
+
constructor(runtime) {
|
|
71
|
+
this.runtime = runtime;
|
|
72
|
+
}
|
|
73
|
+
async call(method, params) {
|
|
74
|
+
const client = this.runtime.client;
|
|
75
|
+
const tStart = Date.now();
|
|
76
|
+
client._clientLog.debug(`call enter: method=${method}`);
|
|
77
|
+
try {
|
|
78
|
+
const preflight = this.preflight(method, params);
|
|
79
|
+
const p = preflight.params;
|
|
80
|
+
const rpcBackground = preflight.rpcBackground;
|
|
81
|
+
const runWithRpcPriority = async (operation) => {
|
|
82
|
+
if (!rpcBackground)
|
|
83
|
+
return await operation();
|
|
84
|
+
return await client._withBackgroundRpc(operation);
|
|
85
|
+
};
|
|
86
|
+
const pullGateLocked = Boolean(p._pull_gate_locked);
|
|
87
|
+
delete p._pull_gate_locked;
|
|
88
|
+
const pullGateKey = this.pullGateKeyForCall(method, p);
|
|
89
|
+
if (pullGateKey && this.isPullResponseProcessing(pullGateKey)) {
|
|
90
|
+
client._clientLog.debug(`pull skipped while processing pull response: method=${method} key=${pullGateKey}`);
|
|
91
|
+
return client._emptyPullResultForCall(method);
|
|
92
|
+
}
|
|
93
|
+
if (pullGateKey && !pullGateLocked) {
|
|
94
|
+
const lockedParams = { ...p, _pull_gate_locked: true };
|
|
95
|
+
if (rpcBackground)
|
|
96
|
+
lockedParams._rpc_background = true;
|
|
97
|
+
return await this.runPullSerialized(pullGateKey, async () => this.call(method, lockedParams));
|
|
98
|
+
}
|
|
99
|
+
if (method === 'message.send') {
|
|
100
|
+
const encrypt = p.encrypt ?? true;
|
|
101
|
+
delete p.encrypt;
|
|
102
|
+
if (encrypt) {
|
|
103
|
+
return await runWithRpcPriority(() => client._sendV2(String(p.to ?? ''), p.payload, {
|
|
104
|
+
messageId: String(p.message_id ?? '') || undefined,
|
|
105
|
+
timestamp: p.timestamp,
|
|
106
|
+
protectedHeaders: client._protectedHeadersFromParams(p),
|
|
107
|
+
context: isJsonObject(p.context) ? p.context : undefined,
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
client._maybeAppendEchoTraceSend(p);
|
|
111
|
+
}
|
|
112
|
+
if (method === 'group.send') {
|
|
113
|
+
const encrypt = p.encrypt ?? true;
|
|
114
|
+
delete p.encrypt;
|
|
115
|
+
if (encrypt) {
|
|
116
|
+
return await runWithRpcPriority(() => client._sendGroupV2(String(p.group_id ?? ''), p.payload, {
|
|
117
|
+
messageId: String(p.message_id ?? '') || undefined,
|
|
118
|
+
timestamp: p.timestamp,
|
|
119
|
+
protectedHeaders: client._protectedHeadersFromParams(p),
|
|
120
|
+
context: isJsonObject(p.context) ? p.context : undefined,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
client._maybeAppendEchoTraceSend(p);
|
|
124
|
+
}
|
|
125
|
+
if (method === 'group.thought.put') {
|
|
126
|
+
const encrypt = p.encrypt ?? true;
|
|
127
|
+
delete p.encrypt;
|
|
128
|
+
if (encrypt) {
|
|
129
|
+
const v2Error = 'V2 session not initialized; encrypted group.thought.put requires V2 (V1 E2EE removed)';
|
|
130
|
+
if (!client._v2Session || !String(p.group_id ?? '').trim()) {
|
|
131
|
+
throw new StateError(v2Error);
|
|
132
|
+
}
|
|
133
|
+
return await runWithRpcPriority(() => client._putGroupThoughtEncryptedV2(p));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (method === 'message.thought.put') {
|
|
137
|
+
const encrypt = p.encrypt ?? true;
|
|
138
|
+
delete p.encrypt;
|
|
139
|
+
if (encrypt) {
|
|
140
|
+
await client._ensureV2SessionReady('message.thought.put', 'V2 session not initialized; encrypted message.thought.put requires V2 (V1 E2EE removed)');
|
|
141
|
+
return await runWithRpcPriority(() => client._putMessageThoughtEncryptedV2(p));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (method === 'message.pull' || method === 'message.v2.pull') {
|
|
145
|
+
await client._ensureV2SessionReady('message.pull');
|
|
146
|
+
const skipAutoAck = p._skip_auto_ack === true || p.skip_auto_ack === true;
|
|
147
|
+
const force = p.force === true;
|
|
148
|
+
const afterSeq = Number(p.after_seq ?? 0) || 0;
|
|
149
|
+
const limit = Number(p.limit ?? 50) || 50;
|
|
150
|
+
const messages = skipAutoAck
|
|
151
|
+
? await runWithRpcPriority(() => client._pullV2(afterSeq, limit, { skipAutoAck: true, gateLocked: true, force }))
|
|
152
|
+
: await runWithRpcPriority(() => client._pullV2(afterSeq, limit, { gateLocked: true, force }));
|
|
153
|
+
return { messages };
|
|
154
|
+
}
|
|
155
|
+
if (method === 'message.ack' || method === 'message.v2.ack') {
|
|
156
|
+
await client._ensureV2SessionReady('message.ack');
|
|
157
|
+
return await runWithRpcPriority(() => client._ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || undefined));
|
|
158
|
+
}
|
|
159
|
+
if (method === 'group.pull' || method === 'group.v2.pull') {
|
|
160
|
+
if (!String(p.group_id ?? '').trim()) {
|
|
161
|
+
throw new ValidationError('group.pull requires group_id');
|
|
162
|
+
}
|
|
163
|
+
await client._ensureV2SessionReady('group.pull');
|
|
164
|
+
const hasExplicitAfterSeq = 'after_seq' in p || 'after_message_seq' in p;
|
|
165
|
+
const cursorParams = client._explicitGroupCursorParams(p);
|
|
166
|
+
const ownsCursor = Object.keys(cursorParams).length === 0 || client._groupCursorTargetsCurrentInstance(cursorParams);
|
|
167
|
+
const pullOpts = { gateLocked: true };
|
|
168
|
+
if (hasExplicitAfterSeq)
|
|
169
|
+
pullOpts.explicitAfterSeq = true;
|
|
170
|
+
if (Object.keys(cursorParams).length > 0)
|
|
171
|
+
pullOpts.cursorParams = cursorParams;
|
|
172
|
+
if (!ownsCursor)
|
|
173
|
+
pullOpts.ownsCursor = false;
|
|
174
|
+
const messages = await runWithRpcPriority(() => client._pullGroupV2(String(p.group_id), Number(p.after_seq ?? p.after_message_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, pullOpts));
|
|
175
|
+
return { messages };
|
|
176
|
+
}
|
|
177
|
+
if (method === 'group.ack_messages' || method === 'group.v2.ack') {
|
|
178
|
+
if (!String(p.group_id ?? '').trim()) {
|
|
179
|
+
throw new ValidationError('group.ack_messages requires group_id');
|
|
180
|
+
}
|
|
181
|
+
await client._ensureV2SessionReady('group.ack_messages');
|
|
182
|
+
const cursorParams = client._explicitGroupCursorParams(p);
|
|
183
|
+
const ownsCursor = Object.keys(cursorParams).length === 0 || client._groupCursorTargetsCurrentInstance(cursorParams);
|
|
184
|
+
if (method === 'group.ack_messages' && !ownsCursor) {
|
|
185
|
+
return await runWithRpcPriority(() => client._rawGroupAckMessages(p));
|
|
186
|
+
}
|
|
187
|
+
return await runWithRpcPriority(() => client._ackGroupV2(String(p.group_id), Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || undefined));
|
|
188
|
+
}
|
|
189
|
+
if (method === 'message.pull') {
|
|
190
|
+
delete p._skip_auto_ack;
|
|
191
|
+
delete p.skip_auto_ack;
|
|
192
|
+
}
|
|
193
|
+
delete p._group_cursor_params;
|
|
194
|
+
this.applyClientSignature(method, p);
|
|
195
|
+
const callTimeout = NON_IDEMPOTENT_METHODS.has(method) ? NON_IDEMPOTENT_TIMEOUT_MS : undefined;
|
|
196
|
+
if (method === 'group.thought.get' || method === 'message.thought.get') {
|
|
197
|
+
client._clientLog.debug(`thought.get transport call start: method=${method}, params=${client._debugJson(client._messageEnvelopeFieldsForDebug(p))}`);
|
|
198
|
+
}
|
|
199
|
+
let result = callTimeout
|
|
200
|
+
? (rpcBackground
|
|
201
|
+
? await client._transport.call(method, p, callTimeout, undefined, true)
|
|
202
|
+
: await client._transport.call(method, p, callTimeout))
|
|
203
|
+
: (rpcBackground
|
|
204
|
+
? await client._transport.call(method, p, undefined, undefined, true)
|
|
205
|
+
: await client._transport.call(method, p));
|
|
206
|
+
result = await this.postprocessResult(method, p, result);
|
|
207
|
+
client._clientLog.debug(`call exit: method=${method} elapsed=${Date.now() - tStart}ms`);
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
client._clientLog.debug(`call exit (error): method=${method} elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
preflight(method, params) {
|
|
216
|
+
const client = this.runtime.client;
|
|
217
|
+
if (client.state !== ConnectionState.READY) {
|
|
218
|
+
throw new ConnectionError('client is not connected');
|
|
219
|
+
}
|
|
220
|
+
if (INTERNAL_ONLY_METHODS.has(method)) {
|
|
221
|
+
throw new PermissionError(`method is internal_only: ${method}`);
|
|
222
|
+
}
|
|
223
|
+
if (method.startsWith('message.e2ee.') || method.startsWith('group.e2ee.') || REMOVED_E2EE_METHODS.has(method)) {
|
|
224
|
+
throw new PermissionError(`legacy E2EE method is removed in this SDK: ${method}`);
|
|
225
|
+
}
|
|
226
|
+
const p = { ...(params ?? {}) };
|
|
227
|
+
this.mergeInstanceProtectedHeaders(method, p);
|
|
228
|
+
const rpcBackground = Boolean(p._rpc_background) || client._backgroundRpcDepth > 0;
|
|
229
|
+
delete p._rpc_background;
|
|
230
|
+
if (method === 'message.send' || method === 'group.send') {
|
|
231
|
+
this.normalizeOutboundMessagePayload(p, method);
|
|
232
|
+
}
|
|
233
|
+
this.validateOutboundCall(method, p);
|
|
234
|
+
this.injectMessageCursorContext(method, p);
|
|
235
|
+
this.captureGroupCursorParams(method, p);
|
|
236
|
+
this.normalizeGroupCallContext(method, p);
|
|
237
|
+
const clampedParams = typeof client._clampAckParams === 'function'
|
|
238
|
+
? client._clampAckParams(method, p)
|
|
239
|
+
: p;
|
|
240
|
+
return { params: clampedParams, rpcBackground };
|
|
241
|
+
}
|
|
242
|
+
mergeInstanceProtectedHeaders(method, params) {
|
|
243
|
+
const client = this.runtime.client;
|
|
244
|
+
if (!client._instanceProtectedHeaders || !PROTECTED_HEADERS_METHODS.has(method)) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const existing = isJsonObject(params.protected_headers) ? params.protected_headers : {};
|
|
248
|
+
params.protected_headers = { ...client._instanceProtectedHeaders, ...existing };
|
|
249
|
+
}
|
|
250
|
+
normalizeOutboundMessagePayload(params, method = '') {
|
|
251
|
+
void method;
|
|
252
|
+
if (!Object.prototype.hasOwnProperty.call(params, 'payload') && Object.prototype.hasOwnProperty.call(params, 'content')) {
|
|
253
|
+
params.payload = params.content;
|
|
254
|
+
delete params.content;
|
|
255
|
+
}
|
|
256
|
+
const payload = params.payload;
|
|
257
|
+
if (isJsonObject(payload) && !Object.prototype.hasOwnProperty.call(payload, 'type') && typeof payload.text === 'string') {
|
|
258
|
+
params.payload = { type: 'text', ...payload };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
validateOutboundCall(method, params) {
|
|
262
|
+
if (method === 'message.send') {
|
|
263
|
+
this.validateMessageRecipient(params.to);
|
|
264
|
+
if ('persist' in params) {
|
|
265
|
+
throw new ValidationError("message.send no longer accepts 'persist'; configure delivery_mode during connect");
|
|
266
|
+
}
|
|
267
|
+
if ('delivery_mode' in params || 'queue_routing' in params || 'affinity_ttl_ms' in params) {
|
|
268
|
+
throw new ValidationError('message.send does not accept delivery_mode; configure delivery_mode during connect');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (method === 'group.send') {
|
|
272
|
+
if ('persist' in params) {
|
|
273
|
+
throw new ValidationError("group.send does not accept 'persist'; group messages are always fanout");
|
|
274
|
+
}
|
|
275
|
+
if ('delivery_mode' in params || 'queue_routing' in params || 'affinity_ttl_ms' in params) {
|
|
276
|
+
throw new ValidationError('group.send does not accept delivery_mode; group messages are always fanout');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (method === 'group.thought.put' || method === 'group.thought.get'
|
|
280
|
+
|| method === 'message.thought.put' || method === 'message.thought.get') {
|
|
281
|
+
const context = isJsonObject(params.context) ? params.context : null;
|
|
282
|
+
const contextType = String(context?.type ?? '').trim();
|
|
283
|
+
const contextId = String(context?.id ?? '').trim();
|
|
284
|
+
const hasContext = contextType.length > 0 && contextId.length > 0;
|
|
285
|
+
if (!hasContext) {
|
|
286
|
+
throw new ValidationError(`${method} requires context.type + context.id`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (method === 'group.thought.get' && !String(params.sender_aid ?? '').trim()) {
|
|
290
|
+
throw new ValidationError('group.thought.get requires sender_aid');
|
|
291
|
+
}
|
|
292
|
+
if (method === 'message.thought.put') {
|
|
293
|
+
this.validateMessageRecipient(params.to);
|
|
294
|
+
if (!String(params.to ?? '').trim()) {
|
|
295
|
+
throw new ValidationError('message.thought.put requires to');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (method === 'message.thought.get' && !String(params.sender_aid ?? '').trim()) {
|
|
299
|
+
throw new ValidationError('message.thought.get requires sender_aid');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
injectMessageCursorContext(method, params) {
|
|
303
|
+
if (method !== 'message.pull' && method !== 'message.ack') {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const client = this.runtime.client;
|
|
307
|
+
if ('device_id' in params && String(params.device_id ?? '').trim() !== client._deviceId) {
|
|
308
|
+
throw new ValidationError('message.pull/message.ack device_id must match the current client instance');
|
|
309
|
+
}
|
|
310
|
+
const slotId = normalizeSlotId(params.slot_id ?? client._slotId, client._slotId);
|
|
311
|
+
if (slotIsolationKey(slotId) !== slotIsolationKey(client._slotId)) {
|
|
312
|
+
throw new ValidationError('message.pull/message.ack slot_id must match the current client instance');
|
|
313
|
+
}
|
|
314
|
+
params.device_id = client._deviceId;
|
|
315
|
+
params.slot_id = client._slotId;
|
|
316
|
+
}
|
|
317
|
+
applyClientSignature(method, params) {
|
|
318
|
+
if (!SIGNED_METHODS.has(method)) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (this.shouldSkipClientSignature(method, params)) {
|
|
322
|
+
delete params.client_signature;
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
this.runtime.client._signClientOperation(method, params);
|
|
326
|
+
}
|
|
327
|
+
shouldSkipClientSignature(method, params) {
|
|
328
|
+
if (method !== 'message.send' && method !== 'group.send')
|
|
329
|
+
return false;
|
|
330
|
+
if (params.encrypted || params.encrypt)
|
|
331
|
+
return false;
|
|
332
|
+
return this.runtime.client._isEchoPayload(params.payload);
|
|
333
|
+
}
|
|
334
|
+
signClientOperation(method, params) {
|
|
335
|
+
const currentAid = this.runtime.client._currentAid;
|
|
336
|
+
if (!currentAid?.privateKeyPem)
|
|
337
|
+
return;
|
|
338
|
+
try {
|
|
339
|
+
const aid = currentAid.aid;
|
|
340
|
+
const ts = String(Math.floor(Date.now() / 1000));
|
|
341
|
+
const paramsForHash = {};
|
|
342
|
+
for (const [k, v] of Object.entries(params)) {
|
|
343
|
+
if (k !== 'client_signature' && !k.startsWith('_')) {
|
|
344
|
+
paramsForHash[k] = v;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const paramsJson = stableStringify(paramsForHash);
|
|
348
|
+
const paramsHash = crypto.createHash('sha256').update(paramsJson, 'utf-8').digest('hex');
|
|
349
|
+
const signData = Buffer.from(`${method}|${aid}|${ts}|${paramsHash}`, 'utf-8');
|
|
350
|
+
const privateKey = crypto.createPrivateKey(currentAid.privateKeyPem);
|
|
351
|
+
const signature = crypto.sign('SHA256', signData, privateKey);
|
|
352
|
+
let certFingerprint = '';
|
|
353
|
+
const certPem = currentAid.certPem;
|
|
354
|
+
if (certPem) {
|
|
355
|
+
const certObj = new crypto.X509Certificate(certPem);
|
|
356
|
+
certFingerprint = 'sha256:' + certObj.fingerprint256.replace(/:/g, '').toLowerCase();
|
|
357
|
+
}
|
|
358
|
+
params.client_signature = {
|
|
359
|
+
aid,
|
|
360
|
+
cert_fingerprint: certFingerprint,
|
|
361
|
+
timestamp: ts,
|
|
362
|
+
params_hash: paramsHash,
|
|
363
|
+
signature: signature.toString('base64'),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
catch (exc) {
|
|
367
|
+
throw new ClientSignatureError(`客户端签名失败,拒绝发送无签名请求: ${formatCaughtError(exc)}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// ── pull-gate ──────────────────────────────────────────────────────────────
|
|
371
|
+
pullGateKeyForCall(method, params) {
|
|
372
|
+
const client = this.runtime.client;
|
|
373
|
+
if (method === 'message.pull' || method === 'message.v2.pull') {
|
|
374
|
+
return client._aid ? `p2p:${client._aid}` : '';
|
|
375
|
+
}
|
|
376
|
+
if ((method === 'group.pull' || method === 'group.v2.pull') && String(params.group_id ?? '').trim()) {
|
|
377
|
+
return `group:${String(params.group_id ?? '').trim()}`;
|
|
378
|
+
}
|
|
379
|
+
if (method === 'group.pull_events' && String(params.group_id ?? '').trim()) {
|
|
380
|
+
return `group_event:${String(params.group_id ?? '').trim()}`;
|
|
381
|
+
}
|
|
382
|
+
return '';
|
|
383
|
+
}
|
|
384
|
+
isPullResponseProcessing(key) {
|
|
385
|
+
if (!key)
|
|
386
|
+
return false;
|
|
387
|
+
return (this.runtime.client._pullResponseKeys.get(key) ?? 0) > 0;
|
|
388
|
+
}
|
|
389
|
+
tryAcquirePullGate(key) {
|
|
390
|
+
if (!key)
|
|
391
|
+
return 0;
|
|
392
|
+
const client = this.runtime.client;
|
|
393
|
+
const now = Date.now();
|
|
394
|
+
const gate = client._pullGates.get(key) ?? { inflight: false, startedAt: 0, token: 0 };
|
|
395
|
+
if (gate.inflight && now - gate.startedAt <= PULL_GATE_STALE_MS) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
if (gate.inflight) {
|
|
399
|
+
client._clientLog.warn(`pull in-flight stale reset: key=${key} age=${now - gate.startedAt}ms`);
|
|
400
|
+
}
|
|
401
|
+
gate.token += 1;
|
|
402
|
+
gate.inflight = true;
|
|
403
|
+
gate.startedAt = now;
|
|
404
|
+
client._pullGates.set(key, gate);
|
|
405
|
+
return gate.token;
|
|
406
|
+
}
|
|
407
|
+
releasePullGate(key, token) {
|
|
408
|
+
if (!key || token == null)
|
|
409
|
+
return;
|
|
410
|
+
const client = this.runtime.client;
|
|
411
|
+
const gate = client._pullGates.get(key);
|
|
412
|
+
if (!gate || gate.token !== token)
|
|
413
|
+
return;
|
|
414
|
+
gate.inflight = false;
|
|
415
|
+
gate.startedAt = 0;
|
|
416
|
+
if (key.startsWith('p2p:')) {
|
|
417
|
+
client._schedulePendingP2pPullIfNeeded(key, 'pull-gate-release');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async runPullSerialized(key, operation) {
|
|
421
|
+
if (key && this.isPullResponseProcessing(key)) {
|
|
422
|
+
this.runtime.client._clientLog.debug(`pull skipped while processing pull response: key=${key}`);
|
|
423
|
+
return [];
|
|
424
|
+
}
|
|
425
|
+
let token = this.tryAcquirePullGate(key);
|
|
426
|
+
if (token === null) {
|
|
427
|
+
const deadline = Date.now() + PULL_GATE_STALE_MS + 100;
|
|
428
|
+
while (token === null && Date.now() <= deadline) {
|
|
429
|
+
await this.runtime.client._sleep(25);
|
|
430
|
+
token = this.tryAcquirePullGate(key);
|
|
431
|
+
}
|
|
432
|
+
if (token === null) {
|
|
433
|
+
throw new StateError(`pull already in-flight for ${key}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
return await this.runtime.client._withBackgroundRpc(operation);
|
|
438
|
+
}
|
|
439
|
+
finally {
|
|
440
|
+
this.releasePullGate(key, token);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// ── raw-call ───────────────────────────────────────────────────────────────
|
|
444
|
+
async rawCall(method, params, opts = {}) {
|
|
445
|
+
const { timeout, trace, signed = true, background = false } = opts;
|
|
446
|
+
const client = this.runtime.client;
|
|
447
|
+
const payload = params ? { ...params } : {};
|
|
448
|
+
if (signed) {
|
|
449
|
+
this.applyClientSignature(method, payload);
|
|
450
|
+
}
|
|
451
|
+
if (background) {
|
|
452
|
+
return await client._transport.call(method, payload, timeout, trace, true);
|
|
453
|
+
}
|
|
454
|
+
if (trace !== undefined) {
|
|
455
|
+
return await client._transport.call(method, payload, timeout, trace);
|
|
456
|
+
}
|
|
457
|
+
if (timeout !== undefined) {
|
|
458
|
+
return await client._transport.call(method, payload, timeout);
|
|
459
|
+
}
|
|
460
|
+
return await client._transport.call(method, payload);
|
|
461
|
+
}
|
|
462
|
+
// ── postprocess ────────────────────────────────────────────────────────────
|
|
463
|
+
async postprocessResult(method, params, result) {
|
|
464
|
+
const client = this.runtime.client;
|
|
465
|
+
let next = result;
|
|
466
|
+
if (method === 'group.thought.get' && isUnknownJsonObject(next)) {
|
|
467
|
+
client._clientLog?.debug?.(`group.thought.get transport result: found=${String(next.found ?? '')}, raw_count=${Array.isArray(next.thoughts) ? next.thoughts.length : 0}`);
|
|
468
|
+
next = await client._decryptGroupThoughts(next);
|
|
469
|
+
}
|
|
470
|
+
if (method === 'message.thought.get' && isUnknownJsonObject(next)) {
|
|
471
|
+
client._clientLog?.debug?.(`message.thought.get transport result: found=${String(next.found ?? '')}, raw_count=${Array.isArray(next.thoughts) ? next.thoughts.length : 0}`);
|
|
472
|
+
next = await client._decryptMessageThoughts(next);
|
|
473
|
+
}
|
|
474
|
+
if (method === 'message.pull' && isUnknownJsonObject(next)) {
|
|
475
|
+
this.postprocessMessagePull(params, next);
|
|
476
|
+
}
|
|
477
|
+
if (method === 'group.pull' && isUnknownJsonObject(next)) {
|
|
478
|
+
this.postprocessGroupPull(params, next);
|
|
479
|
+
}
|
|
480
|
+
next = await client._groupState.postprocessResult(method, params, next);
|
|
481
|
+
return next;
|
|
482
|
+
}
|
|
483
|
+
// ── private helpers ────────────────────────────────────────────────────────
|
|
484
|
+
postprocessMessagePull(params, result) {
|
|
485
|
+
const client = this.runtime.client;
|
|
486
|
+
const messages = result.messages;
|
|
487
|
+
const rawMessages = (Array.isArray(messages) ? messages : []).filter(isJsonObject);
|
|
488
|
+
if (!client._aid || !client._seqTracker) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const ns = `p2p:${client._aid}`;
|
|
492
|
+
const contigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
493
|
+
if (rawMessages.length) {
|
|
494
|
+
client._seqTracker.onPullResult(ns, rawMessages, Number(params.after_seq ?? 0) || 0);
|
|
495
|
+
}
|
|
496
|
+
const serverAck = Number(result.server_ack_seq ?? 0);
|
|
497
|
+
if (serverAck > 0) {
|
|
498
|
+
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
499
|
+
if (contig < serverAck) {
|
|
500
|
+
client._clientLog?.info?.(`message.pull retention-floor advance: ns=${ns} contiguous=${contig} -> server_ack_seq=${serverAck}`);
|
|
501
|
+
client._seqTracker.forceContiguousSeq(ns, serverAck);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (client._seqTracker.getContiguousSeq(ns) !== contigBefore) {
|
|
505
|
+
client._saveSeqTrackerState?.();
|
|
506
|
+
}
|
|
507
|
+
result._contig_before = contigBefore;
|
|
508
|
+
}
|
|
509
|
+
postprocessGroupPull(params, result) {
|
|
510
|
+
const client = this.runtime.client;
|
|
511
|
+
const gid = String(params.group_id ?? '').trim();
|
|
512
|
+
if (!gid || !client._seqTracker) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const messages = result.messages;
|
|
516
|
+
const rawMessages = (Array.isArray(messages) ? messages : []).filter(isJsonObject);
|
|
517
|
+
const ns = `group:${gid}`;
|
|
518
|
+
const contigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
519
|
+
if (rawMessages.length) {
|
|
520
|
+
client._seqTracker.onPullResult(ns, rawMessages, Number(params.after_message_seq ?? params.after_seq ?? 0) || 0);
|
|
521
|
+
}
|
|
522
|
+
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
523
|
+
const serverAck = Number(cursor?.current_seq ?? 0);
|
|
524
|
+
if (serverAck > 0) {
|
|
525
|
+
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
526
|
+
if (contig < serverAck) {
|
|
527
|
+
client._clientLog?.info?.(`group.pull retention-floor advance: ns=${ns} contiguous=${contig} -> cursor.current_seq=${serverAck}`);
|
|
528
|
+
client._seqTracker.forceContiguousSeq(ns, serverAck);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (client._seqTracker.getContiguousSeq(ns) !== contigBefore) {
|
|
532
|
+
client._saveSeqTrackerState?.();
|
|
533
|
+
}
|
|
534
|
+
result._contig_before = contigBefore;
|
|
535
|
+
}
|
|
536
|
+
captureGroupCursorParams(method, params) {
|
|
537
|
+
if (!method.startsWith('group.')
|
|
538
|
+
|| '_group_cursor_params' in params
|
|
539
|
+
|| Boolean(params._pull_gate_locked)) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const explicitCursorParams = this.groupCursorParams(params);
|
|
543
|
+
if (Object.keys(explicitCursorParams).length > 0) {
|
|
544
|
+
params._group_cursor_params = explicitCursorParams;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
normalizeGroupCallContext(method, params) {
|
|
548
|
+
if (!method.startsWith('group.')) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const client = this.runtime.client;
|
|
552
|
+
if (params.group_id !== undefined && params.group_id !== null) {
|
|
553
|
+
const rawGroupId = String(params.group_id);
|
|
554
|
+
const normalizedGroupId = normalizeGroupId(rawGroupId);
|
|
555
|
+
if (normalizedGroupId && normalizedGroupId !== rawGroupId) {
|
|
556
|
+
client._clientLog?.debug?.(`call group_id normalized: ${rawGroupId} -> ${normalizedGroupId} method=${method}`);
|
|
557
|
+
}
|
|
558
|
+
params.group_id = normalizedGroupId;
|
|
559
|
+
}
|
|
560
|
+
if (params.device_id === undefined) {
|
|
561
|
+
params.device_id = client._deviceId;
|
|
562
|
+
}
|
|
563
|
+
if (params.slot_id === undefined) {
|
|
564
|
+
params.slot_id = client._slotId;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
groupCursorParams(params) {
|
|
568
|
+
const cursorParams = {};
|
|
569
|
+
for (const key of ['device_id', 'slot_id', 'device_name', 'device_type']) {
|
|
570
|
+
const value = params[key];
|
|
571
|
+
if (value !== undefined && value !== null)
|
|
572
|
+
cursorParams[key] = value;
|
|
573
|
+
}
|
|
574
|
+
return cursorParams;
|
|
575
|
+
}
|
|
576
|
+
validateMessageRecipient(toAid) {
|
|
577
|
+
if (isGroupServiceAid(toAid)) {
|
|
578
|
+
throw new ValidationError('message.send receiver cannot be group.{issuer}; use group.send instead');
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function isGroupServiceAid(value) {
|
|
583
|
+
const text = String(value ?? '').trim();
|
|
584
|
+
if (!text.includes('.'))
|
|
585
|
+
return false;
|
|
586
|
+
const [name, ...issuerParts] = text.split('.');
|
|
587
|
+
return name === 'group' && issuerParts.join('.').length > 0;
|
|
588
|
+
}
|
|
589
|
+
function stableStringify(obj) {
|
|
590
|
+
if (obj === null || obj === undefined)
|
|
591
|
+
return 'null';
|
|
592
|
+
if (typeof obj === 'boolean' || typeof obj === 'number')
|
|
593
|
+
return JSON.stringify(obj);
|
|
594
|
+
if (typeof obj === 'string')
|
|
595
|
+
return JSON.stringify(obj);
|
|
596
|
+
if (Array.isArray(obj)) {
|
|
597
|
+
return '[' + obj.map(v => stableStringify(v)).join(',') + ']';
|
|
598
|
+
}
|
|
599
|
+
if (isJsonObject(obj)) {
|
|
600
|
+
const keys = Object.keys(obj).sort();
|
|
601
|
+
const entries = keys
|
|
602
|
+
.filter(k => obj[k] !== undefined)
|
|
603
|
+
.map(k => stableStringify(k) + ':' + stableStringify(obj[k]));
|
|
604
|
+
return '{' + entries.join(',') + '}';
|
|
605
|
+
}
|
|
606
|
+
return JSON.stringify(obj);
|
|
607
|
+
}
|
|
608
|
+
function formatCaughtError(error) {
|
|
609
|
+
return error instanceof Error ? error : String(error);
|
|
610
|
+
}
|
|
611
|
+
function isUnknownJsonObject(value) {
|
|
612
|
+
return isJsonObject(value);
|
|
613
|
+
}
|
|
614
|
+
//# sourceMappingURL=rpc-pipeline.js.map
|