@agentunion/fastaun 0.5.0 → 0.5.1
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 +59 -0
- package/_packed_docs/CHANGELOG-validators.md +144 -0
- package/_packed_docs/CHANGELOG.md +59 -0
- package/_packed_docs/INDEX.md +198 -181
- package/_packed_docs/KITE_DOCS_GUIDE.md +26 -23
- package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +4 -3
- package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +331 -0
- package/_packed_docs/protocol/08-AUN-E2EE-Group.md +296 -902
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +64 -114
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +7 -1
- package/_packed_docs/protocol/16-/347/263/273/347/273/237/347/233/256/345/275/225/344/277/235/346/212/244/346/226/271/346/241/210.md +177 -0
- package/_packed_docs/protocol/README.md +2 -1
- package/_packed_docs/protocol/index.md +8 -3
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +4 -252
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +483 -457
- package/_packed_docs/sdk/09-collab-rpc-manual.md +581 -550
- package/_packed_docs/sdk/09-group-rpc-manual.md +248 -335
- package/_packed_docs/sdk/09-storage-rpc-manual.md +56 -19
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +13 -13
- package/_packed_docs/sdk/INDEX.md +14 -14
- package/dist/client/delivery.d.ts +9 -0
- package/dist/client/delivery.js +157 -24
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.js +33 -11
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +1 -0
- package/dist/client/rpc-pipeline.js +127 -40
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +6 -1
- package/dist/client/v2-e2ee.js +199 -130
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +8 -1
- package/dist/client.js +227 -31
- package/dist/client.js.map +1 -1
- package/dist/collab/client.d.ts +8 -0
- package/dist/collab/client.js +12 -0
- package/dist/collab/client.js.map +1 -1
- package/dist/errors.d.ts +0 -20
- package/dist/errors.js +8 -53
- package/dist/errors.js.map +1 -1
- package/dist/facades.d.ts +9 -4
- package/dist/facades.js +137 -15
- package/dist/facades.js.map +1 -1
- package/dist/group-fs.d.ts +26 -9
- package/dist/group-fs.js +84 -84
- package/dist/group-fs.js.map +1 -1
- package/dist/group-id.d.ts +9 -12
- package/dist/group-id.js +41 -63
- package/dist/group-id.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +1 -0
- package/dist/keystore/aid-db.js +43 -43
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/local-identity-store.d.ts +16 -0
- package/dist/keystore/local-identity-store.js +190 -18
- package/dist/keystore/local-identity-store.js.map +1 -1
- package/dist/keystore/local-token-store.d.ts +0 -13
- package/dist/keystore/local-token-store.js +0 -105
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +4 -0
- package/dist/logger.js.map +1 -1
- package/dist/storage/lowlevel.d.ts +9 -1
- package/dist/storage/lowlevel.js +12 -1
- package/dist/storage/lowlevel.js.map +1 -1
- package/dist/storage/types.d.ts +0 -1
- package/dist/storage/types.js.map +1 -1
- package/dist/storage/vfs.d.ts +22 -3
- package/dist/storage/vfs.js +89 -54
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +336 -49
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +2 -0
- package/dist/transport.js +52 -3
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +38 -0
- package/dist/v2/session/session.d.ts +2 -0
- package/dist/v2/session/session.js +53 -23
- package/dist/v2/session/session.js.map +1 -1
- package/dist/v2/state/commitment.d.ts +1 -1
- package/dist/v2/state/commitment.js +4 -2
- package/dist/v2/state/commitment.js.map +1 -1
- package/dist/validators.d.ts +32 -0
- package/dist/validators.js +128 -0
- package/dist/validators.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/_packed_docs/collab-gateway-boundary-test-report.md +0 -164
package/dist/client.js
CHANGED
|
@@ -24,6 +24,7 @@ import { EventDispatcher } from './events.js';
|
|
|
24
24
|
import { LocalTokenStore } from './keystore/local-token-store.js';
|
|
25
25
|
import { AUNLogger } from './logger.js';
|
|
26
26
|
import { normalizeGroupId } from './group-id.js';
|
|
27
|
+
import { validateAIDFormat, validateGroupIDFormat } from './validators.js';
|
|
27
28
|
import { RPCTransport } from './transport.js';
|
|
28
29
|
import { AuthFlow } from './auth.js';
|
|
29
30
|
import { AgentMdManager } from './agent-md.js';
|
|
@@ -138,8 +139,8 @@ const NON_IDEMPOTENT_TIMEOUT_MS = 35_000;
|
|
|
138
139
|
const NON_IDEMPOTENT_METHODS = new Set([
|
|
139
140
|
'message.send', 'group.send', 'group.create', 'group.invite',
|
|
140
141
|
'group.kick', 'group.remove_member', 'group.leave', 'group.dissolve',
|
|
141
|
-
'group.
|
|
142
|
-
'group.
|
|
142
|
+
'group.set_settings',
|
|
143
|
+
'group.update_announcement', 'group.update_rules',
|
|
143
144
|
'storage.put_object', 'storage.delete_object', 'storage.get_by_share',
|
|
144
145
|
'storage.create_share_link', 'storage.revoke_share_link',
|
|
145
146
|
'storage.create_upload_session', 'storage.complete_upload',
|
|
@@ -155,6 +156,7 @@ const NON_IDEMPOTENT_METHODS = new Set([
|
|
|
155
156
|
'storage.fs.invalidate_membership',
|
|
156
157
|
'storage.volume.create', 'storage.volume.renew', 'storage.volume.expire_due',
|
|
157
158
|
'group.fs.mkdir', 'group.fs.rm', 'group.fs.cp', 'group.fs.mv',
|
|
159
|
+
'group.fs.set_acl', 'group.fs.remove_acl',
|
|
158
160
|
'group.fs.mount', 'group.fs.umount',
|
|
159
161
|
'group.fs.check_upload', 'group.fs.create_upload_session',
|
|
160
162
|
'group.fs.complete_upload', 'group.fs.create_download_ticket',
|
|
@@ -178,16 +180,18 @@ const SIGNED_METHODS = new Set([
|
|
|
178
180
|
'group.v2.propose_state', 'group.v2.confirm_state',
|
|
179
181
|
'group.v2.get_proposal',
|
|
180
182
|
'group.kick', 'group.add_member',
|
|
181
|
-
'group.leave', 'group.remove_member',
|
|
182
|
-
'group.update',
|
|
183
|
-
'group.
|
|
184
|
-
'group.transfer_owner', 'group.bind_group_aid', 'group.complete_transfer',
|
|
183
|
+
'group.leave', 'group.remove_member',
|
|
184
|
+
'group.update',
|
|
185
|
+
'group.set_role',
|
|
186
|
+
'group.transfer_owner', 'group.bind_group_aid', 'group.renew_group_aid', 'group.complete_transfer',
|
|
185
187
|
'group.review_join_request',
|
|
186
188
|
'group.batch_review_join_request',
|
|
187
189
|
'group.request_join', 'group.use_invite_code',
|
|
188
190
|
'group.thought.put',
|
|
189
191
|
'message.thought.put',
|
|
190
192
|
'group.set_settings',
|
|
193
|
+
'group.update_announcement',
|
|
194
|
+
'group.update_rules',
|
|
191
195
|
'storage.put_object', 'storage.delete_object', 'storage.get_by_share',
|
|
192
196
|
'storage.create_share_link', 'storage.revoke_share_link',
|
|
193
197
|
'storage.create_upload_session', 'storage.complete_upload',
|
|
@@ -204,6 +208,7 @@ const SIGNED_METHODS = new Set([
|
|
|
204
208
|
'storage.fs.invalidate_membership',
|
|
205
209
|
'storage.volume.create', 'storage.volume.renew', 'storage.volume.expire_due',
|
|
206
210
|
'group.fs.mkdir', 'group.fs.rm', 'group.fs.cp', 'group.fs.mv',
|
|
211
|
+
'group.fs.set_acl', 'group.fs.remove_acl',
|
|
207
212
|
'group.fs.mount', 'group.fs.umount',
|
|
208
213
|
'group.fs.check_upload', 'group.fs.create_upload_session',
|
|
209
214
|
'group.fs.complete_upload', 'group.fs.create_download_ticket',
|
|
@@ -655,10 +660,8 @@ export class AUNClient {
|
|
|
655
660
|
this._dispatcher.subscribe('_raw.group.message_created', (data) => this._onRawGroupMessageCreated(data));
|
|
656
661
|
// 群组消息撤回推送:在线 push 通道,与 pull 双 tombstone 兜底互补(SDK 去重只回调一次)
|
|
657
662
|
this._dispatcher.subscribe('_raw.group.message_recalled', (data) => this._safeAsync(this._onRawGroupMessageRecalled(data)));
|
|
658
|
-
//
|
|
663
|
+
// 群组变更事件:透传并触发 V2 state/SPK 维护
|
|
659
664
|
this._dispatcher.subscribe('_raw.group.changed', (data) => this._onRawGroupChanged(data));
|
|
660
|
-
// V2 epoch 轮换事件:清 bootstrap 缓存并触发 SPK rotation
|
|
661
|
-
this._dispatcher.subscribe('_raw.group.v2.epoch_rotated', (data) => this._safeAsync(this._onV2EpochRotated(data)));
|
|
662
665
|
// V2 state proposal 服务平面事件:owner/admin 负责确认或重新提案
|
|
663
666
|
this._dispatcher.subscribe('_raw.group.v2.state_proposed', (data) => this._safeAsync(this._onV2StateProposed(data)));
|
|
664
667
|
this._dispatcher.subscribe('_raw.group.v2.state_retry_needed', (data) => this._safeAsync(this._onV2StateRetryNeeded(data)));
|
|
@@ -952,7 +955,22 @@ export class AUNClient {
|
|
|
952
955
|
* 自动处理内部方法限制、E2EE 加解密、客户端签名等。
|
|
953
956
|
*/
|
|
954
957
|
async call(method, params) {
|
|
955
|
-
|
|
958
|
+
try {
|
|
959
|
+
return await this._rpcPipeline.call(method, params);
|
|
960
|
+
}
|
|
961
|
+
catch (err) {
|
|
962
|
+
await this._maybeHandleHalfOpenRpcFailure(err);
|
|
963
|
+
throw err;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
async _maybeHandleHalfOpenRpcFailure(err) {
|
|
967
|
+
if (this._closing || this.state !== ConnectionState.READY)
|
|
968
|
+
return;
|
|
969
|
+
if (!this._sessionOptions?.auto_reconnect || this._reconnectActive)
|
|
970
|
+
return;
|
|
971
|
+
if (!(err instanceof ConnectionError) && !(err instanceof TimeoutError))
|
|
972
|
+
return;
|
|
973
|
+
await this._handleTransportDisconnect(err);
|
|
956
974
|
}
|
|
957
975
|
async createGroup(params = {}) {
|
|
958
976
|
if (!isJsonObject(params)) {
|
|
@@ -1007,8 +1025,33 @@ export class AUNClient {
|
|
|
1007
1025
|
if (!store) {
|
|
1008
1026
|
throw new ValidationError('bindGroupAid requires aidStore');
|
|
1009
1027
|
}
|
|
1010
|
-
const
|
|
1028
|
+
const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
|
|
1029
|
+
const keystore = store._keystore; // AIDStore._keystore: LocalIdentityStore
|
|
1030
|
+
let keyPair = null;
|
|
1031
|
+
// 优先复用 pending 槽位中已暂存的待绑定密钥(崩溃/重试幂等)
|
|
1032
|
+
if (groupId && keystore && typeof keystore.loadPendingGroupBind === 'function') {
|
|
1033
|
+
keyPair = keystore.loadPendingGroupBind(groupId);
|
|
1034
|
+
}
|
|
1035
|
+
if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
|
|
1036
|
+
// 未命中 pending,生成新密钥
|
|
1037
|
+
const generated = new CryptoProvider().generateIdentity();
|
|
1038
|
+
keyPair = {
|
|
1039
|
+
private_key_pem: generated.private_key_pem,
|
|
1040
|
+
public_key_der_b64: generated.public_key_der_b64,
|
|
1041
|
+
curve: generated.curve,
|
|
1042
|
+
};
|
|
1043
|
+
// 发 RPC 前先落盘暂存,确保崩溃后重试能复用同一密钥
|
|
1044
|
+
if (groupId && keystore && typeof keystore.savePendingGroupBind === 'function') {
|
|
1045
|
+
keystore.savePendingGroupBind(groupId, keyPair);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if (!keyPair) {
|
|
1049
|
+
throw new ValidationError('bindGroupAid: failed to generate or load key pair');
|
|
1050
|
+
}
|
|
1011
1051
|
const payload = { ...params };
|
|
1052
|
+
if (!String(payload.group_id ?? '').trim() && groupId) {
|
|
1053
|
+
payload.group_id = groupId;
|
|
1054
|
+
}
|
|
1012
1055
|
payload.public_key = keyPair.public_key_der_b64;
|
|
1013
1056
|
payload.curve = keyPair.curve;
|
|
1014
1057
|
const result = await this.call('group.bind_group_aid', payload);
|
|
@@ -1020,6 +1063,8 @@ export class AUNClient {
|
|
|
1020
1063
|
if (!groupAid || !certPem) {
|
|
1021
1064
|
throw new ValidationError('group.bind_group_aid response missing group.group_aid or aid_cert.cert');
|
|
1022
1065
|
}
|
|
1066
|
+
// 证书公钥校验:确保服务端返回的证书公钥 == 本地公钥
|
|
1067
|
+
await this.verifyCertPublicKeyMatch(certPem, keyPair.public_key_der_b64, 'bindGroupAid');
|
|
1023
1068
|
const imported = store.importGroupIdentity(groupAid, {
|
|
1024
1069
|
private_key_pem: keyPair.private_key_pem,
|
|
1025
1070
|
public_key_der_b64: keyPair.public_key_der_b64,
|
|
@@ -1029,6 +1074,10 @@ export class AUNClient {
|
|
|
1029
1074
|
if (!imported.ok) {
|
|
1030
1075
|
throw new ValidationError(imported.error.message);
|
|
1031
1076
|
}
|
|
1077
|
+
// 落盘成功,清除 pending 槽位
|
|
1078
|
+
if (groupId && keystore && typeof keystore.clearPendingGroupBind === 'function') {
|
|
1079
|
+
keystore.clearPendingGroupBind(groupId);
|
|
1080
|
+
}
|
|
1032
1081
|
return result;
|
|
1033
1082
|
}
|
|
1034
1083
|
async startGroupTransfer(params = {}, options = {}) {
|
|
@@ -1046,10 +1095,10 @@ export class AUNClient {
|
|
|
1046
1095
|
}
|
|
1047
1096
|
let groupAid = String(params.group_aid ?? '').trim();
|
|
1048
1097
|
if (!groupAid) {
|
|
1049
|
-
const info = await this.call('group.
|
|
1098
|
+
const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
|
|
1050
1099
|
const infoMap = isJsonObject(info) ? info : {};
|
|
1051
1100
|
const grp = isJsonObject(infoMap.group) ? infoMap.group : infoMap;
|
|
1052
|
-
groupAid = String(
|
|
1101
|
+
groupAid = String(info?.group_aid ?? '').trim();
|
|
1053
1102
|
}
|
|
1054
1103
|
if (!groupAid) {
|
|
1055
1104
|
throw new ValidationError('startGroupTransfer: unable to determine group_aid');
|
|
@@ -1080,6 +1129,99 @@ export class AUNClient {
|
|
|
1080
1129
|
payload.transfer_auth = { nonce, issued_ms: issuedMs, signature: signed.data.signature };
|
|
1081
1130
|
return await this.call('group.transfer_owner', payload);
|
|
1082
1131
|
}
|
|
1132
|
+
async renewGroupAid(params = {}, options = {}) {
|
|
1133
|
+
if (!isJsonObject(params)) {
|
|
1134
|
+
throw new ValidationError('renewGroupAid params must be an object');
|
|
1135
|
+
}
|
|
1136
|
+
const store = options.aidStore;
|
|
1137
|
+
if (!store) {
|
|
1138
|
+
throw new ValidationError('renewGroupAid requires aidStore');
|
|
1139
|
+
}
|
|
1140
|
+
const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
|
|
1141
|
+
if (!groupId) {
|
|
1142
|
+
throw new ValidationError('renewGroupAid requires group_id or group_aid');
|
|
1143
|
+
}
|
|
1144
|
+
let groupAid = String(params.group_aid ?? '').trim();
|
|
1145
|
+
if (!groupAid) {
|
|
1146
|
+
const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
|
|
1147
|
+
const infoMap = isJsonObject(info) ? info : {};
|
|
1148
|
+
const grp = isJsonObject(infoMap.group) ? infoMap.group : infoMap;
|
|
1149
|
+
groupAid = String(info?.group_aid ?? '').trim();
|
|
1150
|
+
}
|
|
1151
|
+
if (!groupAid) {
|
|
1152
|
+
throw new ValidationError('renewGroupAid: unable to determine group_aid');
|
|
1153
|
+
}
|
|
1154
|
+
// 加载旧 group_aid 私钥(用于签名授权轮换)
|
|
1155
|
+
const loaded = store.load(groupAid);
|
|
1156
|
+
if (!loaded.ok || !loaded.data) {
|
|
1157
|
+
throw new ValidationError(`renewGroupAid: group_aid identity not found: ${groupAid}`);
|
|
1158
|
+
}
|
|
1159
|
+
const oldAidObj = loaded.data.aid;
|
|
1160
|
+
if (!oldAidObj.isPrivateKeyValid() || !oldAidObj.privateKeyPem) {
|
|
1161
|
+
throw new ValidationError(`renewGroupAid: group_aid has no private key: ${groupAid}`);
|
|
1162
|
+
}
|
|
1163
|
+
const oldPublicKey = oldAidObj.publicKey;
|
|
1164
|
+
if (!oldPublicKey) {
|
|
1165
|
+
throw new ValidationError(`renewGroupAid: cannot determine old public key for ${groupAid}`);
|
|
1166
|
+
}
|
|
1167
|
+
// 生成新密钥
|
|
1168
|
+
const newKeyPair = new CryptoProvider().generateIdentity();
|
|
1169
|
+
const newPublicKey = newKeyPair.public_key_der_b64;
|
|
1170
|
+
const newPrivateKey = newKeyPair.private_key_pem;
|
|
1171
|
+
const curve = newKeyPair.curve || 'P-256';
|
|
1172
|
+
if (!newPublicKey || !newPrivateKey) {
|
|
1173
|
+
throw new ValidationError('renewGroupAid: generated incomplete group identity');
|
|
1174
|
+
}
|
|
1175
|
+
// 用旧私钥对 canonical payload 签名
|
|
1176
|
+
const nonce = crypto.randomUUID().replace(/-/g, '');
|
|
1177
|
+
const issuedMs = Date.now();
|
|
1178
|
+
const oldHash = crypto.createHash('sha256').update(oldPublicKey).digest('hex');
|
|
1179
|
+
const newHash = crypto.createHash('sha256').update(newPublicKey).digest('hex');
|
|
1180
|
+
const canonical = [
|
|
1181
|
+
'aun-group-aid-renew-v1',
|
|
1182
|
+
groupId.toLowerCase(),
|
|
1183
|
+
groupAid.toLowerCase(),
|
|
1184
|
+
oldHash,
|
|
1185
|
+
newHash,
|
|
1186
|
+
nonce,
|
|
1187
|
+
String(issuedMs),
|
|
1188
|
+
].join('|');
|
|
1189
|
+
const signed = oldAidObj.sign(canonical);
|
|
1190
|
+
if (!signed.ok) {
|
|
1191
|
+
throw new ValidationError(`renewGroupAid: sign failed: ${signed.error.message}`);
|
|
1192
|
+
}
|
|
1193
|
+
const payload = { ...params };
|
|
1194
|
+
if (!String(payload.group_id ?? '').trim()) {
|
|
1195
|
+
payload.group_id = groupId;
|
|
1196
|
+
}
|
|
1197
|
+
payload.group_aid = groupAid;
|
|
1198
|
+
payload.old_public_key = oldPublicKey;
|
|
1199
|
+
payload.new_public_key = newPublicKey;
|
|
1200
|
+
payload.curve = curve;
|
|
1201
|
+
payload.renew_proof = { nonce, issued_ms: issuedMs, signature: signed.data.signature };
|
|
1202
|
+
const result = await this.call('group.renew_group_aid', payload);
|
|
1203
|
+
const resultMap = isJsonObject(result) ? result : {};
|
|
1204
|
+
const group = isJsonObject(resultMap.group) ? resultMap.group : {};
|
|
1205
|
+
const aidCert = isJsonObject(resultMap.aid_cert) ? resultMap.aid_cert : {};
|
|
1206
|
+
const returnedGroupAid = String(group.group_aid ?? resultMap.group_aid ?? groupAid).trim();
|
|
1207
|
+
const certPem = String(aidCert.cert ?? aidCert.cert_pem ?? resultMap.cert ?? resultMap.cert_pem ?? '').trim();
|
|
1208
|
+
if (!returnedGroupAid || !certPem) {
|
|
1209
|
+
throw new ValidationError('renewGroupAid response missing group.group_aid or aid_cert.cert');
|
|
1210
|
+
}
|
|
1211
|
+
// 证书公钥校验:确保服务端返回的新证书公钥 == 本地新公钥
|
|
1212
|
+
await this.verifyCertPublicKeyMatch(certPem, newPublicKey, 'renewGroupAid');
|
|
1213
|
+
// 用新密钥覆盖本地身份(合法轮换)
|
|
1214
|
+
const imported = store.importGroupIdentity(returnedGroupAid, {
|
|
1215
|
+
private_key_pem: newPrivateKey,
|
|
1216
|
+
public_key_der_b64: newPublicKey,
|
|
1217
|
+
curve,
|
|
1218
|
+
cert_pem: certPem,
|
|
1219
|
+
});
|
|
1220
|
+
if (!imported.ok) {
|
|
1221
|
+
throw new ValidationError(`renewGroupAid failed to persist group identity: ${imported.error.message}`);
|
|
1222
|
+
}
|
|
1223
|
+
return result;
|
|
1224
|
+
}
|
|
1083
1225
|
async completeGroupTransfer(params = {}, options = {}) {
|
|
1084
1226
|
if (!isJsonObject(params)) {
|
|
1085
1227
|
throw new ValidationError('completeGroupTransfer params must be an object');
|
|
@@ -1096,10 +1238,10 @@ export class AUNClient {
|
|
|
1096
1238
|
}
|
|
1097
1239
|
let groupAid = String(params.group_aid ?? '').trim();
|
|
1098
1240
|
if (!groupAid) {
|
|
1099
|
-
const info = await this.call('group.
|
|
1241
|
+
const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
|
|
1100
1242
|
const infoMap = isJsonObject(info) ? info : {};
|
|
1101
1243
|
const grp = isJsonObject(infoMap.group) ? infoMap.group : infoMap;
|
|
1102
|
-
groupAid = String(
|
|
1244
|
+
groupAid = String(info?.group_aid ?? '').trim();
|
|
1103
1245
|
}
|
|
1104
1246
|
if (!groupAid) {
|
|
1105
1247
|
throw new ValidationError('completeGroupTransfer: unable to determine group_aid');
|
|
@@ -1138,6 +1280,8 @@ export class AUNClient {
|
|
|
1138
1280
|
if (!returnedGroupAid || !certPem) {
|
|
1139
1281
|
throw new ValidationError('group.complete_transfer response missing group.group_aid or aid_cert.cert');
|
|
1140
1282
|
}
|
|
1283
|
+
// 证书公钥校验:确保服务端返回的证书公钥 == 本地公钥
|
|
1284
|
+
await this.verifyCertPublicKeyMatch(certPem, keyPair.public_key_der_b64, 'completeGroupTransfer');
|
|
1141
1285
|
const imported = store.importGroupIdentity(returnedGroupAid, {
|
|
1142
1286
|
private_key_pem: keyPair.private_key_pem,
|
|
1143
1287
|
public_key_der_b64: keyPair.public_key_der_b64,
|
|
@@ -1149,6 +1293,32 @@ export class AUNClient {
|
|
|
1149
1293
|
}
|
|
1150
1294
|
return result;
|
|
1151
1295
|
}
|
|
1296
|
+
/** 证书公钥校验:确保服务端返回的证书公钥 == 本地公钥(DER base64) */
|
|
1297
|
+
async verifyCertPublicKeyMatch(certPem, localPublicKeyDerB64, operation) {
|
|
1298
|
+
// 简化实现:使用 Node.js crypto 或浏览器 Web Crypto API
|
|
1299
|
+
// 完整实现需要解析 X.509 证书提取 subjectPublicKeyInfo
|
|
1300
|
+
// 这里先做基本校验,后续可增强
|
|
1301
|
+
const crypto = globalThis.crypto;
|
|
1302
|
+
if (!crypto || !crypto.subtle) {
|
|
1303
|
+
// 浏览器环境或 Node < 15,跳过校验(非致命)
|
|
1304
|
+
console.warn(`${operation}: crypto.subtle not available, skipping cert public key verification`);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
try {
|
|
1308
|
+
// TODO: 完整实现需要 ASN.1 解析库提取证书公钥
|
|
1309
|
+
// 当前仅做导入测试,确保证书格式有效
|
|
1310
|
+
const certLines = certPem.split('\n').filter(l => l && !l.startsWith('-----'));
|
|
1311
|
+
const certDer = Buffer.from(certLines.join(''), 'base64');
|
|
1312
|
+
// 验证本地公钥可导入
|
|
1313
|
+
const localPubDer = Buffer.from(localPublicKeyDerB64, 'base64');
|
|
1314
|
+
await crypto.subtle.importKey('spki', localPubDer, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
|
|
1315
|
+
// 证书公钥提取需要完整 ASN.1 解析,暂时依赖服务端正确性
|
|
1316
|
+
// 后续迭代可引入 @peculiar/x509 或类似库
|
|
1317
|
+
}
|
|
1318
|
+
catch (err) {
|
|
1319
|
+
throw new AuthError(`${operation}: certificate or public key validation failed: ${err.message}`);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1152
1322
|
static _notifyParamsSizeOk(params) {
|
|
1153
1323
|
return new TextEncoder().encode(JSON.stringify(params)).length <= MAX_NOTIFY_PAYLOAD_SIZE;
|
|
1154
1324
|
}
|
|
@@ -1183,7 +1353,7 @@ export class AUNClient {
|
|
|
1183
1353
|
throw new ValidationError('notify payload is too large');
|
|
1184
1354
|
}
|
|
1185
1355
|
const targetAid = String(options.to ?? '').trim();
|
|
1186
|
-
const targetGroupId = String(options.group_id ?? options.groupId ?? '').trim();
|
|
1356
|
+
const targetGroupId = String(options.group_aid ?? options.groupAid ?? options.group_id ?? options.groupId ?? '').trim();
|
|
1187
1357
|
const targetDeviceId = String(options.device_id ?? options.deviceId ?? '').trim();
|
|
1188
1358
|
const targetSlotId = String(options.slot_id ?? options.slotId ?? '').trim();
|
|
1189
1359
|
const ttl = AUNClient._normalizeNotifyTtl(options.ttl_ms ?? options.ttlMs);
|
|
@@ -1193,6 +1363,13 @@ export class AUNClient {
|
|
|
1193
1363
|
if (targetSlotId && !targetDeviceId) {
|
|
1194
1364
|
throw new ValidationError('slot_id requires device_id for notify target');
|
|
1195
1365
|
}
|
|
1366
|
+
// 校验 AID 和 Group ID 格式
|
|
1367
|
+
if (targetAid) {
|
|
1368
|
+
validateAIDFormat(targetAid, 'to');
|
|
1369
|
+
}
|
|
1370
|
+
if (targetGroupId) {
|
|
1371
|
+
validateGroupIDFormat(targetGroupId, 'group_id');
|
|
1372
|
+
}
|
|
1196
1373
|
if (targetAid) {
|
|
1197
1374
|
const eventMethod = AUNClient._validateNotifyEventMethod(method);
|
|
1198
1375
|
const target = { type: 'aid', aid: targetAid };
|
|
@@ -1216,6 +1393,7 @@ export class AUNClient {
|
|
|
1216
1393
|
throw new ValidationError('group_id is required for group notify');
|
|
1217
1394
|
}
|
|
1218
1395
|
const routeParams = {
|
|
1396
|
+
group_aid: normalizedGroupId,
|
|
1219
1397
|
group_id: normalizedGroupId,
|
|
1220
1398
|
deliver: { method: eventMethod, params: payload },
|
|
1221
1399
|
};
|
|
@@ -1250,8 +1428,11 @@ export class AUNClient {
|
|
|
1250
1428
|
delete p._skip_auto_ack;
|
|
1251
1429
|
delete p.skip_auto_ack;
|
|
1252
1430
|
delete p._group_cursor_params;
|
|
1253
|
-
if (method.startsWith('group.') && p.
|
|
1254
|
-
p.
|
|
1431
|
+
if (method.startsWith('group.') && p.group_aid !== undefined && p.group_aid !== null) {
|
|
1432
|
+
p.group_aid = normalizeGroupId(String(p.group_aid)) || String(p.group_aid);
|
|
1433
|
+
}
|
|
1434
|
+
else if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null) {
|
|
1435
|
+
p.group_aid = normalizeGroupId(String(p.group_id)) || String(p.group_id);
|
|
1255
1436
|
}
|
|
1256
1437
|
if (method.startsWith('group.') && p.device_id === undefined) {
|
|
1257
1438
|
p.device_id = this._deviceId;
|
|
@@ -1411,7 +1592,13 @@ export class AUNClient {
|
|
|
1411
1592
|
_messageEnvelopeFieldsForDebug(message) {
|
|
1412
1593
|
return this._delivery.messageEnvelopeFieldsForDebug(message);
|
|
1413
1594
|
}
|
|
1595
|
+
_isMessageDebugEnabled() {
|
|
1596
|
+
const checker = this._clientLog.isDebugEnabled;
|
|
1597
|
+
return typeof checker === 'function' ? checker.call(this._clientLog) : true;
|
|
1598
|
+
}
|
|
1414
1599
|
_logMessageDebug(stage, source, event, message, opts = {}) {
|
|
1600
|
+
if (!this._isMessageDebugEnabled())
|
|
1601
|
+
return;
|
|
1415
1602
|
this._delivery.logMessageDebug(stage, source, event, message, opts);
|
|
1416
1603
|
}
|
|
1417
1604
|
_tryAcquirePullGate(key) {
|
|
@@ -1546,21 +1733,21 @@ export class AUNClient {
|
|
|
1546
1733
|
}
|
|
1547
1734
|
_extractGroupIdFromResult(result) {
|
|
1548
1735
|
const group = isJsonObject(result.group) ? result.group : null;
|
|
1549
|
-
const gid = group ? String(group.group_id ?? '') : '';
|
|
1736
|
+
const gid = group ? String(group.group_aid ?? group.group_id ?? '') : '';
|
|
1550
1737
|
if (gid)
|
|
1551
1738
|
return gid;
|
|
1552
|
-
const directGid = String(result.group_id ?? '');
|
|
1739
|
+
const directGid = String(result.group_aid ?? result.group_id ?? '');
|
|
1553
1740
|
if (directGid)
|
|
1554
1741
|
return directGid;
|
|
1555
1742
|
const member = isJsonObject(result.member) ? result.member : null;
|
|
1556
|
-
return member ? String(member.group_id ?? '') : '';
|
|
1743
|
+
return member ? String(member.group_aid ?? member.group_id ?? '') : '';
|
|
1557
1744
|
}
|
|
1558
1745
|
async _onRawGroupChanged(data) {
|
|
1559
1746
|
const tStart = Date.now();
|
|
1560
1747
|
try {
|
|
1561
1748
|
if (isJsonObject(data)) {
|
|
1562
1749
|
const d = data;
|
|
1563
|
-
const groupId = String(d.group_id ?? '');
|
|
1750
|
+
const groupId = String(d.group_aid ?? d.group_id ?? '');
|
|
1564
1751
|
const action = String(d.action ?? '');
|
|
1565
1752
|
this._clientLog.debug(`_onRawGroupChanged enter: group_id=${groupId}, action=${action}, event_seq=${String(d.event_seq ?? '')}`);
|
|
1566
1753
|
// 验签:有 client_signature 就验,没有默认安全(H20: 严格 boolean)
|
|
@@ -1598,18 +1785,22 @@ export class AUNClient {
|
|
|
1598
1785
|
this._v2E2EE.deleteBootstrapCacheEntry(`group:${groupId}`);
|
|
1599
1786
|
this._v2GroupSecurityLevels.delete(groupId);
|
|
1600
1787
|
this._v2StateChains.delete(groupId);
|
|
1601
|
-
|
|
1602
|
-
|
|
1788
|
+
const groupNs = `group:${groupId}`;
|
|
1789
|
+
const groupEventNs = `group_event:${groupId}`;
|
|
1790
|
+
this._seqTracker.removeNamespace(groupNs);
|
|
1791
|
+
this._seqTracker.removeNamespace(groupEventNs);
|
|
1792
|
+
this._persistRepairedSeq(groupNs);
|
|
1793
|
+
this._persistRepairedSeq(groupEventNs);
|
|
1603
1794
|
this._saveSeqTrackerState();
|
|
1604
1795
|
for (const key of this._gapFillDone.keys()) {
|
|
1605
1796
|
if (key.includes(groupId)) {
|
|
1606
1797
|
this._gapFillDone.delete(key);
|
|
1607
1798
|
}
|
|
1608
1799
|
}
|
|
1609
|
-
this._pushedSeqs.delete(
|
|
1610
|
-
this._pushedSeqs.delete(
|
|
1611
|
-
this._pendingOrderedMsgs.delete(
|
|
1612
|
-
this._pendingOrderedMsgs.delete(
|
|
1800
|
+
this._pushedSeqs.delete(groupNs);
|
|
1801
|
+
this._pushedSeqs.delete(groupEventNs);
|
|
1802
|
+
this._pendingOrderedMsgs.delete(groupNs);
|
|
1803
|
+
this._pendingOrderedMsgs.delete(groupEventNs);
|
|
1613
1804
|
this._clientLog.info(`cleaned up disbanded group ${groupId} local state`);
|
|
1614
1805
|
}
|
|
1615
1806
|
/** 同步验签群事件 client_signature。返回 true/false/"pending"。 */
|
|
@@ -1867,6 +2058,7 @@ export class AUNClient {
|
|
|
1867
2058
|
return JSON.stringify([this._aid, this._deviceId, this._slotId]);
|
|
1868
2059
|
}
|
|
1869
2060
|
_resetSeqTrackingState() {
|
|
2061
|
+
this._saveSeqTrackerState();
|
|
1870
2062
|
this._resetV2IdentityRuntime();
|
|
1871
2063
|
this._seqTracker = new SeqTracker();
|
|
1872
2064
|
this._seqTrackerContext = null;
|
|
@@ -1888,6 +2080,7 @@ export class AUNClient {
|
|
|
1888
2080
|
const nextContext = this._currentSeqTrackerContext();
|
|
1889
2081
|
if (nextContext === this._seqTrackerContext)
|
|
1890
2082
|
return;
|
|
2083
|
+
this._saveSeqTrackerState();
|
|
1891
2084
|
this._seqTracker = new SeqTracker();
|
|
1892
2085
|
this._gapFillDone.clear();
|
|
1893
2086
|
this._pushedSeqs.clear();
|
|
@@ -1908,6 +2101,9 @@ export class AUNClient {
|
|
|
1908
2101
|
_saveSeqTrackerState() {
|
|
1909
2102
|
return this._delivery.saveSeqTrackerState();
|
|
1910
2103
|
}
|
|
2104
|
+
_persistSeq(ns, forceSeq) {
|
|
2105
|
+
return this._delivery.persistSeq(ns, forceSeq);
|
|
2106
|
+
}
|
|
1911
2107
|
_persistRepairedSeq(ns) {
|
|
1912
2108
|
return this._delivery.persistRepairedSeq(ns);
|
|
1913
2109
|
}
|
|
@@ -2573,9 +2769,6 @@ export class AUNClient {
|
|
|
2573
2769
|
async _decryptV2PushMessage(data) {
|
|
2574
2770
|
return await this._v2E2EE.decryptV2PushMessage(data);
|
|
2575
2771
|
}
|
|
2576
|
-
async _onV2EpochRotated(data) {
|
|
2577
|
-
return this._v2E2EE.handleV2EpochRotated(data);
|
|
2578
|
-
}
|
|
2579
2772
|
/** 按当前 AID 发现 Gateway;用于 authenticate()/connect() 的新入口。 */
|
|
2580
2773
|
async _resolveGatewayForAid(aid) {
|
|
2581
2774
|
const resolvedAid = String(aid ?? this._aid ?? '').trim();
|
|
@@ -3007,6 +3200,9 @@ export class AUNClient {
|
|
|
3007
3200
|
this._clientLog.warn(`transport disconnected: closeCode=${closeCode ?? 'none'}, error=${error ? formatCaughtError(error) : 'none'}`);
|
|
3008
3201
|
this._state = 'standby';
|
|
3009
3202
|
this._stopBackgroundTasks();
|
|
3203
|
+
void this._transport.close().catch((exc) => {
|
|
3204
|
+
this._clientLog.debug(`transport cleanup skipped: ${formatCaughtError(exc)}`);
|
|
3205
|
+
});
|
|
3010
3206
|
await this._dispatcher.publish('state_change', { state: this._publicState(this._state), error });
|
|
3011
3207
|
if (!this._sessionOptions.auto_reconnect) {
|
|
3012
3208
|
this._reconnectActive = false;
|