@agentunion/fastaun 0.2.14 → 0.2.15

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/dist/client.d.ts CHANGED
@@ -15,6 +15,7 @@ import { GroupE2EEManager } from './e2ee-group.js';
15
15
  import { type Subscription, type EventHandler } from './events.js';
16
16
  import { AuthNamespace } from './namespaces/auth.js';
17
17
  import { CustodyNamespace } from './namespaces/custody.js';
18
+ import { MetaNamespace } from './namespaces/meta.js';
18
19
  import { type JsonValue, type RpcParams, type RpcResult } from './types.js';
19
20
  /**
20
21
  * 递归排序键的 JSON 序列化(Canonical JSON for AUN)
@@ -58,6 +59,8 @@ export declare class AUNClient {
58
59
  readonly auth: AuthNamespace;
59
60
  /** AID 托管命名空间 */
60
61
  readonly custody: CustodyNamespace;
62
+ /** Meta 命名空间(心跳、状态、信任根管理) */
63
+ readonly meta: MetaNamespace;
61
64
  /** 会话参数(重连用) */
62
65
  private _sessionParams;
63
66
  /** 会话选项 */
@@ -149,6 +152,7 @@ export declare class AUNClient {
149
152
  on(event: string, handler: EventHandler): Subscription;
150
153
  /** P2-13: 取消订阅事件(对齐 Python/JS off 方法) */
151
154
  off(event: string, handler: EventHandler): void;
155
+ private _protectedHeadersFromParams;
152
156
  /** 自动加密并发送 P2P 消息 */
153
157
  private _sendEncrypted;
154
158
  private _sendEncryptedSingle;
package/dist/client.js CHANGED
@@ -27,6 +27,7 @@ import { AUNLogger } from './logger.js';
27
27
  import { SQLiteBackup } from './keystore/sqlite-backup.js';
28
28
  import { AuthNamespace } from './namespaces/auth.js';
29
29
  import { CustodyNamespace } from './namespaces/custody.js';
30
+ import { MetaNamespace } from './namespaces/meta.js';
30
31
  import { RPCTransport } from './transport.js';
31
32
  import { AuthFlow } from './auth.js';
32
33
  import { SeqTracker } from './seq-tracker.js';
@@ -141,6 +142,7 @@ const SIGNED_METHODS = new Set([
141
142
  ]);
142
143
  /** peer 证书缓存 TTL(10 分钟) */
143
144
  const PEER_CERT_CACHE_TTL = 600;
145
+ const PREKEY_FALLBACK_DEVICE_ID = 'aun_device_id';
144
146
  function isGroupServiceAid(value) {
145
147
  const text = String(value ?? '').trim();
146
148
  if (!text.includes('.'))
@@ -165,6 +167,52 @@ function isPeerPrekeyResponse(value) {
165
167
  return false;
166
168
  return candidate.prekey === undefined || isPeerPrekeyMaterial(candidate.prekey);
167
169
  }
170
+ function normalizePeerPrekeys(prekeys) {
171
+ const normalized = [];
172
+ for (const item of prekeys) {
173
+ if (!isPeerPrekeyMaterial(item))
174
+ continue;
175
+ const prekeyId = item.prekey_id.trim();
176
+ const publicKey = item.public_key.trim();
177
+ const signature = item.signature.trim();
178
+ if (!prekeyId || !publicKey || !signature)
179
+ continue;
180
+ const deviceId = String(item.device_id ?? '').trim();
181
+ const certFingerprint = String(item.cert_fingerprint ?? '').trim().toLowerCase();
182
+ const candidate = {
183
+ ...item,
184
+ prekey_id: prekeyId,
185
+ public_key: publicKey,
186
+ signature,
187
+ device_id: deviceId,
188
+ };
189
+ if (certFingerprint) {
190
+ candidate.cert_fingerprint = certFingerprint;
191
+ }
192
+ else {
193
+ delete candidate.cert_fingerprint;
194
+ }
195
+ normalized.push(candidate);
196
+ }
197
+ if (normalized.length === 0)
198
+ return [];
199
+ if (normalized.length === 1) {
200
+ if (!String(normalized[0].device_id ?? '').trim()) {
201
+ normalized[0].device_id = PREKEY_FALLBACK_DEVICE_ID;
202
+ }
203
+ return normalized;
204
+ }
205
+ const seen = new Set();
206
+ const filtered = [];
207
+ for (const item of normalized) {
208
+ const deviceId = String(item.device_id ?? '').trim();
209
+ if (!deviceId || deviceId === PREKEY_FALLBACK_DEVICE_ID || seen.has(deviceId))
210
+ continue;
211
+ seen.add(deviceId);
212
+ filtered.push(item);
213
+ }
214
+ return filtered;
215
+ }
168
216
  function formatCaughtError(error) {
169
217
  return error instanceof Error ? error : String(error);
170
218
  }
@@ -271,6 +319,8 @@ export class AUNClient {
271
319
  auth;
272
320
  /** AID 托管命名空间 */
273
321
  custody;
322
+ /** Meta 命名空间(心跳、状态、信任根管理) */
323
+ meta;
274
324
  /** 会话参数(重连用) */
275
325
  _sessionParams = null;
276
326
  /** 会话选项 */
@@ -368,6 +418,7 @@ export class AUNClient {
368
418
  });
369
419
  this.auth = new AuthNamespace(this);
370
420
  this.custody = new CustodyNamespace(this);
421
+ this.meta = new MetaNamespace(this);
371
422
  // 内部订阅:推送消息自动解密后 re-publish 给用户
372
423
  this._dispatcher.subscribe('_raw.message.received', (data) => this._onRawMessageReceived(data));
373
424
  // 群组消息推送:自动解密后 re-publish
@@ -518,6 +569,8 @@ export class AUNClient {
518
569
  if (encrypt) {
519
570
  return await this._sendEncrypted(p);
520
571
  }
572
+ delete p.protected_headers;
573
+ delete p.headers;
521
574
  }
522
575
  // 自动加密:group.send 默认加密(encrypt 默认 True)
523
576
  if (method === 'group.send') {
@@ -526,6 +579,8 @@ export class AUNClient {
526
579
  if (encrypt) {
527
580
  return await this._sendGroupEncrypted(p);
528
581
  }
582
+ delete p.protected_headers;
583
+ delete p.headers;
529
584
  }
530
585
  if (method === 'group.thought.put') {
531
586
  const encrypt = p.encrypt ?? true;
@@ -698,6 +753,17 @@ export class AUNClient {
698
753
  this._dispatcher.unsubscribe(event, handler);
699
754
  }
700
755
  // ── E2EE 加密发送 ────────────────────────────────────────
756
+ _protectedHeadersFromParams(params) {
757
+ const value = params.protected_headers ?? params.headers;
758
+ if (value == null)
759
+ return null;
760
+ if (isJsonObject(value))
761
+ return value;
762
+ if (typeof value === 'object' && typeof value.toObject === 'function') {
763
+ return value;
764
+ }
765
+ return null;
766
+ }
701
767
  /** 自动加密并发送 P2P 消息 */
702
768
  async _sendEncrypted(params) {
703
769
  const toAid = String(params.to ?? '');
@@ -709,6 +775,7 @@ export class AUNClient {
709
775
  throw new ValidationError('message.send payload must be an object when encrypt=true');
710
776
  }
711
777
  const persistRequired = Boolean(params.persist_required || params.durable);
778
+ const protectedHeaders = this._protectedHeadersFromParams(params);
712
779
  // 惰性同步:首次发送 P2P 消息时先 pull 一次
713
780
  if (!this._p2pSynced) {
714
781
  await this._lazySyncP2p();
@@ -719,6 +786,7 @@ export class AUNClient {
719
786
  payload,
720
787
  messageId,
721
788
  timestamp,
789
+ protectedHeaders,
722
790
  });
723
791
  if (recipientPrekeys.length <= 1 && selfSyncCopies.length === 0) {
724
792
  return await this._sendEncryptedSingle({
@@ -728,6 +796,7 @@ export class AUNClient {
728
796
  timestamp,
729
797
  prekey: recipientPrekeys[0],
730
798
  persistRequired,
799
+ protectedHeaders,
731
800
  });
732
801
  }
733
802
  const recipientCopies = await this._buildRecipientDeviceCopies({
@@ -736,6 +805,7 @@ export class AUNClient {
736
805
  messageId,
737
806
  timestamp,
738
807
  prekeys: recipientPrekeys,
808
+ protectedHeaders,
739
809
  });
740
810
  const sendParams = {
741
811
  to: toAid,
@@ -769,6 +839,7 @@ export class AUNClient {
769
839
  prekey,
770
840
  messageId: opts.messageId,
771
841
  timestamp: opts.timestamp,
842
+ protectedHeaders: opts.protectedHeaders,
772
843
  });
773
844
  this._ensureEncryptResult(opts.toAid, encryptResult);
774
845
  const sendParams = {
@@ -787,7 +858,7 @@ export class AUNClient {
787
858
  async _buildRecipientDeviceCopies(opts) {
788
859
  const recipientCopies = [];
789
860
  const certCache = new Map();
790
- for (const prekey of opts.prekeys) {
861
+ for (const prekey of normalizePeerPrekeys(opts.prekeys)) {
791
862
  const deviceId = String(prekey.device_id ?? '').trim();
792
863
  const peerCertFingerprint = String(prekey.cert_fingerprint ?? '').trim().toLowerCase();
793
864
  const cacheKey = peerCertFingerprint || '__default__';
@@ -803,6 +874,7 @@ export class AUNClient {
803
874
  prekey,
804
875
  messageId: opts.messageId,
805
876
  timestamp: opts.timestamp,
877
+ protectedHeaders: opts.protectedHeaders,
806
878
  });
807
879
  this._ensureEncryptResult(opts.toAid, encryptResult);
808
880
  recipientCopies.push({
@@ -845,7 +917,7 @@ export class AUNClient {
845
917
  if (!myAid) {
846
918
  return [];
847
919
  }
848
- const prekeys = await this._fetchPeerPrekeys(myAid);
920
+ const prekeys = normalizePeerPrekeys(await this._fetchPeerPrekeys(myAid));
849
921
  if (prekeys.length === 0) {
850
922
  return [];
851
923
  }
@@ -863,6 +935,7 @@ export class AUNClient {
863
935
  prekey,
864
936
  messageId: opts.messageId,
865
937
  timestamp: opts.timestamp,
938
+ protectedHeaders: opts.protectedHeaders,
866
939
  });
867
940
  this._ensureEncryptResult(myAid, encryptResult);
868
941
  copies.push({
@@ -873,7 +946,7 @@ export class AUNClient {
873
946
  return copies;
874
947
  }
875
948
  _encryptCopyPayload(opts) {
876
- const [envelope, encryptResult] = this._e2ee.encryptOutbound(opts.logicalToAid, opts.payload, opts.peerCertPem, opts.prekey ?? null, opts.messageId, opts.timestamp);
949
+ const [envelope, encryptResult] = this._e2ee.encryptOutbound(opts.logicalToAid, opts.payload, opts.peerCertPem, opts.prekey ?? null, opts.messageId, opts.timestamp, opts.protectedHeaders, opts.context ?? null);
877
950
  return [envelope, encryptResult];
878
951
  }
879
952
  _ensureEncryptResult(toAid, encryptResult) {
@@ -905,7 +978,7 @@ export class AUNClient {
905
978
  return await this._callGroupEncryptedRpc('group.thought.put', params, {
906
979
  idField: 'thought_id',
907
980
  idPrefix: 'gt',
908
- extraFields: ['reply_to'],
981
+ extraFields: ['context'],
909
982
  });
910
983
  }
911
984
  async _putMessageThoughtEncrypted(params) {
@@ -930,6 +1003,8 @@ export class AUNClient {
930
1003
  prekey,
931
1004
  messageId: thoughtId,
932
1005
  timestamp,
1006
+ protectedHeaders: this._protectedHeadersFromParams(params),
1007
+ context: isJsonObject(params.context) ? params.context : null,
933
1008
  });
934
1009
  this._ensureEncryptResult(toAid, encryptResult);
935
1010
  const sendParams = {
@@ -939,8 +1014,9 @@ export class AUNClient {
939
1014
  encrypted: true,
940
1015
  thought_id: thoughtId,
941
1016
  timestamp,
942
- reply_to: params.reply_to,
943
1017
  };
1018
+ if ('context' in params)
1019
+ sendParams.context = params.context;
944
1020
  this._signClientOperation('message.thought.put', sendParams);
945
1021
  return await this._transport.call('message.thought.put', sendParams);
946
1022
  }
@@ -977,12 +1053,26 @@ export class AUNClient {
977
1053
  await this._waitForGroupMembershipEpochFloor(groupId, 2000);
978
1054
  const epochResult = await this._committedGroupEpochState(groupId);
979
1055
  const committedEpoch = Number(epochResult.committed_epoch ?? epochResult.epoch ?? 0);
980
- const envelope = committedEpoch > 0
981
- ? this._groupE2ee.encryptWithEpoch(groupId, await this._ensureCommittedGroupSecretForSend(groupId, committedEpoch, epochResult), payload)
982
- : await this._groupE2ee.encrypt(groupId, payload);
983
1056
  const operationId = String(params[options.idField] ?? '').trim()
984
1057
  || `${options.idPrefix}-${crypto.randomUUID()}`;
985
1058
  const timestamp = Number(params.timestamp ?? Date.now());
1059
+ const protectedHeaders = this._protectedHeadersFromParams(params);
1060
+ const context = method === 'group.thought.put' && isJsonObject(params.context)
1061
+ ? params.context
1062
+ : null;
1063
+ const envelope = committedEpoch > 0
1064
+ ? this._groupE2ee.encryptWithEpoch(groupId, await this._ensureCommittedGroupSecretForSend(groupId, committedEpoch, epochResult), payload, {
1065
+ messageId: operationId,
1066
+ timestamp,
1067
+ protectedHeaders,
1068
+ context,
1069
+ })
1070
+ : await this._groupE2ee.encrypt(groupId, payload, {
1071
+ messageId: operationId,
1072
+ timestamp,
1073
+ protectedHeaders,
1074
+ context,
1075
+ });
986
1076
  const sendParams = {
987
1077
  group_id: groupId,
988
1078
  payload: envelope,
@@ -2416,11 +2506,17 @@ export class AUNClient {
2416
2506
  async _fetchPeerPrekeys(peerAid) {
2417
2507
  const cachedList = this._peerPrekeysCache.get(peerAid);
2418
2508
  if (cachedList && Date.now() / 1000 < cachedList.expireAt) {
2419
- return cachedList.items.map((item) => ({ ...item }));
2509
+ const normalized = normalizePeerPrekeys(cachedList.items);
2510
+ if (normalized.length > 0) {
2511
+ return normalized.map((item) => ({ ...item }));
2512
+ }
2420
2513
  }
2421
2514
  const cached = this._e2ee.getCachedPrekey(peerAid);
2422
- if (cached !== null)
2423
- return [{ ...cached }];
2515
+ if (cached !== null) {
2516
+ const normalized = normalizePeerPrekeys([cached]);
2517
+ if (normalized.length > 0)
2518
+ return normalized.map((item) => ({ ...item }));
2519
+ }
2424
2520
  try {
2425
2521
  const result = await this._transport.call('message.e2ee.get_prekey', { aid: peerAid });
2426
2522
  if (!isJsonObject(result)) {
@@ -2431,13 +2527,7 @@ export class AUNClient {
2431
2527
  }
2432
2528
  const devicePrekeys = Array.isArray(result.device_prekeys) ? result.device_prekeys : null;
2433
2529
  if (devicePrekeys) {
2434
- const normalized = [];
2435
- for (const item of devicePrekeys) {
2436
- if (!isPeerPrekeyMaterial(item)) {
2437
- continue;
2438
- }
2439
- normalized.push({ ...item });
2440
- }
2530
+ const normalized = normalizePeerPrekeys(devicePrekeys);
2441
2531
  if (normalized.length > 0) {
2442
2532
  this._peerPrekeysCache.set(peerAid, {
2443
2533
  items: normalized.map((item) => ({ ...item })),
@@ -2452,12 +2542,15 @@ export class AUNClient {
2452
2542
  }
2453
2543
  const prekey = result.prekey;
2454
2544
  if (prekey) {
2455
- this._peerPrekeysCache.set(peerAid, {
2456
- items: [{ ...prekey }],
2457
- expireAt: Date.now() / 1000 + 300,
2458
- });
2459
- this._e2ee.cachePrekey(peerAid, prekey);
2460
- return [{ ...prekey }];
2545
+ const normalized = normalizePeerPrekeys([prekey]);
2546
+ if (normalized.length > 0) {
2547
+ this._peerPrekeysCache.set(peerAid, {
2548
+ items: normalized.map((item) => ({ ...item })),
2549
+ expireAt: Date.now() / 1000 + 300,
2550
+ });
2551
+ this._e2ee.cachePrekey(peerAid, normalized[0]);
2552
+ return normalized.map((item) => ({ ...item }));
2553
+ }
2461
2554
  }
2462
2555
  if (result.found) {
2463
2556
  throw new ValidationError(`invalid prekey response for ${peerAid}`);
@@ -2475,7 +2568,10 @@ export class AUNClient {
2475
2568
  async _fetchPeerPrekey(peerAid) {
2476
2569
  const cachedList = this._peerPrekeysCache.get(peerAid);
2477
2570
  if (cachedList && Date.now() / 1000 < cachedList.expireAt && cachedList.items.length > 0) {
2478
- return { ...cachedList.items[0] };
2571
+ const normalized = normalizePeerPrekeys(cachedList.items);
2572
+ if (normalized.length > 0) {
2573
+ return { ...normalized[0] };
2574
+ }
2479
2575
  }
2480
2576
  const prekeys = await this._fetchPeerPrekeys(peerAid);
2481
2577
  if (prekeys.length === 0) {
@@ -2852,14 +2948,16 @@ export class AUNClient {
2852
2948
  this._enqueuePendingDecrypt(groupId, message);
2853
2949
  continue;
2854
2950
  }
2855
- thoughts.push({
2951
+ const thought = {
2856
2952
  thought_id: thoughtId,
2857
2953
  message_id: thoughtId,
2858
- reply_to: item.reply_to,
2859
2954
  payload: decrypted.payload,
2860
2955
  created_at: item.created_at,
2861
2956
  e2ee: decrypted.e2ee,
2862
- });
2957
+ };
2958
+ if ('context' in item)
2959
+ thought.context = item.context;
2960
+ thoughts.push(thought);
2863
2961
  }
2864
2962
  return { ...result, thoughts };
2865
2963
  }
@@ -2902,16 +3000,18 @@ export class AUNClient {
2902
3000
  continue;
2903
3001
  }
2904
3002
  }
2905
- thoughts.push({
3003
+ const thought = {
2906
3004
  thought_id: thoughtId,
2907
3005
  message_id: thoughtId,
2908
- reply_to: item.reply_to,
2909
3006
  from: fromAid,
2910
3007
  to: toAid,
2911
3008
  payload: decrypted.payload,
2912
3009
  created_at: item.created_at,
2913
3010
  e2ee: decrypted.e2ee,
2914
- });
3011
+ };
3012
+ if ('context' in item)
3013
+ thought.context = item.context;
3014
+ thoughts.push(thought);
2915
3015
  }
2916
3016
  return { ...result, thoughts };
2917
3017
  }
@@ -3997,10 +4097,12 @@ export class AUNClient {
3997
4097
  }
3998
4098
  if (method === 'group.thought.put' || method === 'group.thought.get'
3999
4099
  || method === 'message.thought.put' || method === 'message.thought.get') {
4000
- const replyTo = isJsonObject(params.reply_to) ? params.reply_to : null;
4001
- const replyMsgId = String(replyTo?.message_id ?? '').trim();
4002
- if (!replyMsgId) {
4003
- throw new ValidationError(`${method} requires reply_to.message_id`);
4100
+ const context = isJsonObject(params.context) ? params.context : null;
4101
+ const contextType = String(context?.type ?? '').trim();
4102
+ const contextId = String(context?.id ?? '').trim();
4103
+ const hasContext = contextType.length > 0 && contextId.length > 0;
4104
+ if (!hasContext) {
4105
+ throw new ValidationError(`${method} requires context.type + context.id`);
4004
4106
  }
4005
4107
  }
4006
4108
  if (method === 'group.thought.get' && !String(params.sender_aid ?? '').trim()) {