@agentunion/fastaun-browser 0.5.0 → 0.5.2

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.
Files changed (140) hide show
  1. package/CHANGELOG.md +550 -464
  2. package/_packed_docs/CHANGELOG-validators.md +147 -0
  3. package/_packed_docs/CHANGELOG.md +550 -464
  4. package/_packed_docs/INDEX.md +201 -177
  5. package/_packed_docs/KITE_DOCS_GUIDE.md +38 -32
  6. package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +169 -116
  7. package/_packed_docs/aun-perf-audit-critical-bugs.md +315 -0
  8. package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +263 -260
  9. package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +331 -0
  10. package/_packed_docs/protocol/00-/346/200/273/350/247/210/344/270/216/345/210/206/345/261/202.md +2 -2
  11. package/_packed_docs/protocol/00A-/350/256/276/350/256/241/345/216/237/345/210/231-/344/270/272Agent/350/200/214/347/224/237.md +1 -1
  12. package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +39 -16
  13. package/_packed_docs/protocol/03-Gateway-/350/277/236/346/216/245/346/250/241/345/274/217.md +8 -5
  14. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +18 -19
  15. package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +1 -1
  16. package/_packed_docs/protocol/08-AUN-E2EE-Group.md +139 -746
  17. package/_packed_docs/protocol/08-AUN-E2EE.md +12 -10
  18. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +117 -171
  19. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +6 -0
  20. package/_packed_docs/protocol/15-/347/246/273/347/272/277/346/216/250/351/200/201/351/200/232/347/237/245/345/215/217/350/256/256.md +1 -1
  21. 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
  22. package/_packed_docs/protocol/README.md +5 -4
  23. package/_packed_docs/protocol/aun-docs-guide.md +2 -2
  24. package/_packed_docs/protocol/index.md +12 -7
  25. package/_packed_docs/protocol//350/215/211/346/241/210-/346/213/222/347/273/235/344/277/241/345/217/267/345/215/217/350/256/256.md +1 -1
  26. package/_packed_docs/protocol//351/231/204/345/275/225A-/346/234/257/350/257/255/350/241/250.md +13 -13
  27. package/_packed_docs/protocol//351/231/204/345/275/225L-E2EE/345/256/236/347/216/260/346/214/207/345/215/227.md +9 -9
  28. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +15 -13
  29. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +26 -18
  30. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +35 -284
  31. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +473 -442
  32. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +11 -7
  33. package/_packed_docs/sdk/09-collab-rpc-manual.md +581 -550
  34. package/_packed_docs/sdk/09-group-rpc-manual.md +367 -433
  35. package/_packed_docs/sdk/09-message-rpc-manual.md +50 -28
  36. package/_packed_docs/sdk/09-payload-reference.md +3 -3
  37. package/_packed_docs/sdk/09-storage-rpc-manual.md +57 -20
  38. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +18 -17
  39. package/_packed_docs/sdk/E2EE_V2/346/266/210/346/201/257/351/200/232/344/277/241/346/227/266/345/272/217/345/233/276.md +3 -2
  40. package/_packed_docs/sdk/INDEX.md +25 -24
  41. package/_packed_docs/sdk/Notify/351/200/232/347/237/245/346/226/271/346/241/210.md +6 -2
  42. package/dist/agent-md.d.ts.map +1 -1
  43. package/dist/agent-md.js +18 -9
  44. package/dist/agent-md.js.map +1 -1
  45. package/dist/bundle.js +1661 -1180
  46. package/dist/client/delivery.d.ts +13 -2
  47. package/dist/client/delivery.d.ts.map +1 -1
  48. package/dist/client/delivery.js +251 -46
  49. package/dist/client/delivery.js.map +1 -1
  50. package/dist/client/group-state.d.ts.map +1 -1
  51. package/dist/client/group-state.js +36 -14
  52. package/dist/client/group-state.js.map +1 -1
  53. package/dist/client/lifecycle.js +2 -2
  54. package/dist/client/lifecycle.js.map +1 -1
  55. package/dist/client/rpc-pipeline.d.ts +1 -0
  56. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  57. package/dist/client/rpc-pipeline.js +166 -50
  58. package/dist/client/rpc-pipeline.js.map +1 -1
  59. package/dist/client/v2-e2ee.d.ts +14 -1
  60. package/dist/client/v2-e2ee.d.ts.map +1 -1
  61. package/dist/client/v2-e2ee.js +376 -126
  62. package/dist/client/v2-e2ee.js.map +1 -1
  63. package/dist/client.d.ts +5 -4
  64. package/dist/client.d.ts.map +1 -1
  65. package/dist/client.js +187 -46
  66. package/dist/client.js.map +1 -1
  67. package/dist/collab/client.d.ts +8 -0
  68. package/dist/collab/client.d.ts.map +1 -1
  69. package/dist/collab/client.js +12 -0
  70. package/dist/collab/client.js.map +1 -1
  71. package/dist/errors.d.ts +0 -20
  72. package/dist/errors.d.ts.map +1 -1
  73. package/dist/errors.js +8 -51
  74. package/dist/errors.js.map +1 -1
  75. package/dist/facades.d.ts +9 -4
  76. package/dist/facades.d.ts.map +1 -1
  77. package/dist/facades.js +192 -31
  78. package/dist/facades.js.map +1 -1
  79. package/dist/group-fs.d.ts +17 -0
  80. package/dist/group-fs.d.ts.map +1 -1
  81. package/dist/group-fs.js +54 -11
  82. package/dist/group-fs.js.map +1 -1
  83. package/dist/group-id.d.ts +9 -12
  84. package/dist/group-id.d.ts.map +1 -1
  85. package/dist/group-id.js +41 -63
  86. package/dist/group-id.js.map +1 -1
  87. package/dist/index.d.ts +4 -2
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +4 -1
  90. package/dist/index.js.map +1 -1
  91. package/dist/keystore/index.d.ts +2 -54
  92. package/dist/keystore/index.d.ts.map +1 -1
  93. package/dist/keystore/indexeddb-identity-store.d.ts +3 -0
  94. package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
  95. package/dist/keystore/indexeddb-identity-store.js +65 -0
  96. package/dist/keystore/indexeddb-identity-store.js.map +1 -1
  97. package/dist/keystore/indexeddb-shared.d.ts +3 -17
  98. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  99. package/dist/keystore/indexeddb-shared.js +4 -47
  100. package/dist/keystore/indexeddb-shared.js.map +1 -1
  101. package/dist/keystore/indexeddb-token-store.d.ts +1 -64
  102. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  103. package/dist/keystore/indexeddb-token-store.js +45 -774
  104. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  105. package/dist/logger.d.ts +2 -0
  106. package/dist/logger.d.ts.map +1 -1
  107. package/dist/logger.js +4 -0
  108. package/dist/logger.js.map +1 -1
  109. package/dist/storage/lowlevel.d.ts +9 -1
  110. package/dist/storage/lowlevel.d.ts.map +1 -1
  111. package/dist/storage/lowlevel.js +12 -1
  112. package/dist/storage/lowlevel.js.map +1 -1
  113. package/dist/storage/vfs.d.ts +22 -0
  114. package/dist/storage/vfs.d.ts.map +1 -1
  115. package/dist/storage/vfs.js +54 -0
  116. package/dist/storage/vfs.js.map +1 -1
  117. package/dist/tools/cross-sdk-agent.js +336 -49
  118. package/dist/tools/cross-sdk-agent.js.map +1 -1
  119. package/dist/transport.d.ts +2 -0
  120. package/dist/transport.d.ts.map +1 -1
  121. package/dist/transport.js +96 -3
  122. package/dist/transport.js.map +1 -1
  123. package/dist/types.d.ts +39 -56
  124. package/dist/types.d.ts.map +1 -1
  125. package/dist/v2/session/session.d.ts +2 -0
  126. package/dist/v2/session/session.d.ts.map +1 -1
  127. package/dist/v2/session/session.js +58 -22
  128. package/dist/v2/session/session.js.map +1 -1
  129. package/dist/v2/state/commitment.d.ts +1 -1
  130. package/dist/v2/state/commitment.d.ts.map +1 -1
  131. package/dist/v2/state/commitment.js +5 -3
  132. package/dist/v2/state/commitment.js.map +1 -1
  133. package/dist/validators.d.ts +35 -0
  134. package/dist/validators.d.ts.map +1 -0
  135. package/dist/validators.js +127 -0
  136. package/dist/validators.js.map +1 -0
  137. package/dist/version.d.ts +1 -1
  138. package/dist/version.js +1 -1
  139. package/package.json +1 -1
  140. package/_packed_docs/collab-gateway-boundary-test-report.md +0 -164
package/dist/bundle.js CHANGED
@@ -460,7 +460,7 @@ var init_indexeddb_store = __esm({
460
460
  });
461
461
 
462
462
  // src/version.ts
463
- var VERSION = "0.5.0";
463
+ var VERSION = "0.5.2";
464
464
 
465
465
  // src/types.ts
466
466
  var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
@@ -621,36 +621,6 @@ var E2EEDecryptFailedError = class extends E2EEError {
621
621
  this.name = "E2EEDecryptFailedError";
622
622
  }
623
623
  };
624
- var E2EEGroupSecretMissingError = class extends E2EEError {
625
- constructor(message = "group secret missing", opts) {
626
- super(message, { ...opts, code: -32040, localCode: "E2EE_GROUP_SECRET_MISSING" });
627
- this.name = "E2EEGroupSecretMissingError";
628
- }
629
- };
630
- var E2EEGroupEpochMismatchError = class extends E2EEError {
631
- constructor(message = "group epoch mismatch", opts) {
632
- super(message, { ...opts, code: -32041, localCode: "E2EE_GROUP_EPOCH_MISMATCH" });
633
- this.name = "E2EEGroupEpochMismatchError";
634
- }
635
- };
636
- var E2EEGroupCommitmentInvalidError = class extends E2EEError {
637
- constructor(message = "group commitment invalid", opts) {
638
- super(message, { ...opts, code: -32042, localCode: "E2EE_GROUP_COMMITMENT_INVALID" });
639
- this.name = "E2EEGroupCommitmentInvalidError";
640
- }
641
- };
642
- var E2EEGroupNotMemberError = class extends E2EEError {
643
- constructor(message = "not a group member", opts) {
644
- super(message, { ...opts, code: -32043, localCode: "E2EE_GROUP_NOT_MEMBER" });
645
- this.name = "E2EEGroupNotMemberError";
646
- }
647
- };
648
- var E2EEGroupDecryptFailedError = class extends E2EEError {
649
- constructor(message = "group message decrypt failed", opts) {
650
- super(message, { ...opts, code: -32044, localCode: "E2EE_GROUP_DECRYPT_FAILED" });
651
- this.name = "E2EEGroupDecryptFailedError";
652
- }
653
- };
654
624
  var CertificateRevokedError = class extends AuthError {
655
625
  constructor(message = "peer certificate has been revoked", opts) {
656
626
  super(message, { ...opts, code: -32050 });
@@ -669,6 +639,10 @@ var ClientSignatureError = class extends ValidationError {
669
639
  this.name = "ClientSignatureError";
670
640
  }
671
641
  };
642
+ function isTransientGatewayDegradedError(message) {
643
+ const text3 = message.trim().toLowerCase();
644
+ return text3.includes("gateway service degraded") || text3.includes("certificate not loaded");
645
+ }
672
646
  function mapRemoteError(error) {
673
647
  const code = Number(error.code ?? -32603);
674
648
  const message = String(error.message ?? "remote error");
@@ -705,20 +679,10 @@ function mapRemoteError(error) {
705
679
  Cls = GroupStateError;
706
680
  } else if (code >= -33009 && code <= -33004) {
707
681
  Cls = GroupError;
708
- } else if (code === -32040) {
709
- Cls = E2EEGroupSecretMissingError;
710
- } else if (code === -32041) {
711
- Cls = E2EEGroupEpochMismatchError;
712
- } else if (code === -32042) {
713
- Cls = E2EEGroupCommitmentInvalidError;
714
- } else if (code === -32043) {
715
- Cls = E2EEGroupNotMemberError;
716
- } else if (code === -32044) {
717
- Cls = E2EEGroupDecryptFailedError;
718
682
  } else {
719
683
  Cls = AUNError;
720
684
  }
721
- const retryable = Cls === RateLimitError || code >= 5e3 && code < 6e3;
685
+ const retryable = Cls === RateLimitError || code >= 5e3 && code < 6e3 || isTransientGatewayDegradedError(message);
722
686
  return new Cls(message, { code, data, retryable, traceId });
723
687
  }
724
688
 
@@ -879,55 +843,172 @@ var EventDispatcher = class {
879
843
  };
880
844
 
881
845
  // src/group-id.ts
882
- function normalizeGroupId(raw, opts) {
883
- const value = String(raw ?? "").trim().toLowerCase();
846
+ function trimDots(value) {
847
+ return value.replace(/^\.+|\.+$/g, "");
848
+ }
849
+ function convertToGroupAid(raw, opts) {
850
+ const value = String(raw ?? "").trim().replace(/^\/+|\/+$/g, "").toLowerCase();
884
851
  if (!value) return "";
885
852
  if (value.startsWith("group.") && value.includes("/")) {
886
853
  const issuerAndBase = value.slice(6);
887
854
  const slashIdx = issuerAndBase.indexOf("/");
888
855
  if (slashIdx > 0 && slashIdx < issuerAndBase.length - 1) {
889
- const aDomain = issuerAndBase.slice(0, slashIdx).replace(/^\.+|\.+$/g, "");
890
- const baseTail = issuerAndBase.slice(slashIdx + 1);
856
+ const domain = trimDots(issuerAndBase.slice(0, slashIdx));
857
+ const baseTail = issuerAndBase.slice(slashIdx + 1).replace(/^\/+|\/+$/g, "");
891
858
  if (baseTail.includes("@")) {
892
859
  const atIdx = baseTail.indexOf("@");
893
- const base = baseTail.slice(0, atIdx).replace(/^\.+|\.+$/g, "");
894
- const bDomain = baseTail.slice(atIdx + 1).replace(/^\.+|\.+$/g, "");
895
- if (base && bDomain) {
896
- const merged = aDomain ? `${bDomain}.${aDomain}` : bDomain;
897
- return `group.${merged}/${base}`;
860
+ const base2 = trimDots(baseTail.slice(0, atIdx));
861
+ const suffixDomain = trimDots(baseTail.slice(atIdx + 1));
862
+ if (base2 && suffixDomain) {
863
+ const merged = domain ? `${suffixDomain}.${domain}` : suffixDomain;
864
+ return `${base2}.${merged}`;
898
865
  }
899
866
  }
900
- return aDomain ? `group.${aDomain}/${baseTail.replace(/^\.+|\.+$/g, "")}` : value;
867
+ const base = trimDots(baseTail);
868
+ if (base && domain) return `${base}.${domain}`;
869
+ return value;
901
870
  }
902
871
  return value;
903
872
  }
904
873
  if (value.includes("@")) {
905
874
  const atIdx = value.indexOf("@");
906
- const base = value.slice(0, atIdx).replace(/^\.+|\.+$/g, "");
907
- const domain = value.slice(atIdx + 1).replace(/^\.+|\.+$/g, "");
908
- if (base && domain) return `group.${domain}/${base}`;
909
- return value;
910
- }
911
- const localIssuer = (opts?.localIssuer ?? "").trim().replace(/^\.+|\.+$/g, "").toLowerCase();
912
- if (value.startsWith("g-")) {
913
- const rest = value.slice(2);
914
- const dotIdx2 = rest.indexOf(".");
915
- if (dotIdx2 > 0) {
916
- const slug = rest.slice(0, dotIdx2);
917
- const domain = rest.slice(dotIdx2 + 1).replace(/^\.+|\.+$/g, "");
918
- if (slug && domain) return `group.${domain}/g-${slug}`;
919
- }
920
- if (localIssuer) return `group.${localIssuer}/g-${rest}`;
875
+ const base = trimDots(value.slice(0, atIdx));
876
+ const domain = trimDots(value.slice(atIdx + 1));
877
+ if (base && domain) return `${base}.${domain}`;
921
878
  return value;
922
879
  }
923
- const dotIdx = value.indexOf(".");
880
+ if (value.includes(".")) return value;
881
+ const localIssuer = trimDots((opts?.localIssuer ?? "").trim().toLowerCase());
882
+ if (localIssuer) return `${value}.${localIssuer}`;
883
+ return value;
884
+ }
885
+ function normalizeGroupAid(raw, opts) {
886
+ return convertToGroupAid(raw, opts);
887
+ }
888
+ function normalizeGroupId(raw, opts) {
889
+ return convertToGroupAid(raw, opts);
890
+ }
891
+ function splitGroupId(raw) {
892
+ const groupAid = convertToGroupAid(raw);
893
+ const dotIdx = groupAid.indexOf(".");
924
894
  if (dotIdx > 0) {
925
- const base = value.slice(0, dotIdx);
926
- const domain = value.slice(dotIdx + 1).replace(/^\.+|\.+$/g, "");
927
- if (base && domain) return `group.${domain}/${base}`;
895
+ return {
896
+ base: trimDots(groupAid.slice(0, dotIdx)),
897
+ domain: trimDots(groupAid.slice(dotIdx + 1))
898
+ };
928
899
  }
929
- if (localIssuer) return `group.${localIssuer}/${value}`;
930
- return value;
900
+ return { base: trimDots(groupAid), domain: "" };
901
+ }
902
+ function buildDiscoveryHost(raw) {
903
+ const { domain } = splitGroupId(raw);
904
+ return domain;
905
+ }
906
+
907
+ // src/validators.ts
908
+ var AID_NAME_RE = /^[a-z0-9_][a-z0-9_-]{3,63}$/;
909
+ var GROUP_ID_LEGACY_PATTERN = /^g-[a-z0-9]{4,32}$/;
910
+ var GROUP_ID_NEW_BASE_PATTERN = /^[a-z0-9]{5,64}$/;
911
+ var GROUP_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{3,63}$/;
912
+ var DOMAIN_RE = /^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/;
913
+ function validateAIDFormat(aid, paramName = "aid") {
914
+ if (aid === null || aid === void 0 || typeof aid === "string" && aid.trim() === "") {
915
+ throw new ValidationError(`${paramName} cannot be empty`);
916
+ }
917
+ const aidStr = String(aid).trim().toLowerCase();
918
+ if (!aidStr.includes(".")) {
919
+ throw new ValidationError(
920
+ `Invalid ${paramName} '${String(aid)}': must be in format '{name}.{issuer}'`
921
+ );
922
+ }
923
+ const dotIdx = aidStr.indexOf(".");
924
+ const name = aidStr.slice(0, dotIdx);
925
+ const issuer = aidStr.slice(dotIdx + 1);
926
+ if (!name) {
927
+ throw new ValidationError(`Invalid ${paramName} '${String(aid)}': name part cannot be empty`);
928
+ }
929
+ if (!AID_NAME_RE.test(name)) {
930
+ throw new ValidationError(
931
+ `Invalid ${paramName} '${String(aid)}': name '${name}' must be 4-64 characters, only [a-z0-9_-], cannot start with '-'`
932
+ );
933
+ }
934
+ if (name.startsWith("guest")) {
935
+ throw new ValidationError(
936
+ `Invalid ${paramName} '${String(aid)}': name cannot start with 'guest'`
937
+ );
938
+ }
939
+ if (!issuer) {
940
+ throw new ValidationError(`Invalid ${paramName} '${String(aid)}': issuer part cannot be empty`);
941
+ }
942
+ if (!DOMAIN_RE.test(issuer)) {
943
+ throw new ValidationError(
944
+ `Invalid ${paramName} '${String(aid)}': issuer '${issuer}' is not a valid domain`
945
+ );
946
+ }
947
+ return aidStr;
948
+ }
949
+ function resolveGroupValidationArgs(defaultParamName, paramNameOrOptions, maybeOptions) {
950
+ if (typeof paramNameOrOptions === "string") {
951
+ return {
952
+ paramName: paramNameOrOptions || defaultParamName,
953
+ localIssuer: maybeOptions?.localIssuer ?? ""
954
+ };
955
+ }
956
+ return {
957
+ paramName: paramNameOrOptions?.paramName ?? defaultParamName,
958
+ localIssuer: paramNameOrOptions?.localIssuer ?? ""
959
+ };
960
+ }
961
+ function validateGroupAidParts(raw, groupAid, paramName) {
962
+ const stripDots = (s) => s.replace(/^\.+|\.+$/g, "");
963
+ const dotIdx = groupAid.indexOf(".");
964
+ const base = dotIdx >= 0 ? stripDots(groupAid.slice(0, dotIdx)) : groupAid;
965
+ const domain = dotIdx >= 0 ? stripDots(groupAid.slice(dotIdx + 1)) : "";
966
+ if (!base) {
967
+ throw new ValidationError(
968
+ `Invalid ${paramName} '${String(raw)}': base part cannot be empty`
969
+ );
970
+ }
971
+ const isValidBase = GROUP_ID_LEGACY_PATTERN.test(base) || GROUP_ID_NEW_BASE_PATTERN.test(base) || GROUP_NAME_PATTERN.test(base);
972
+ if (!isValidBase) {
973
+ throw new ValidationError(
974
+ `Invalid ${paramName} '${String(raw)}': base '${base}' must be one of: legacy format 'g-[a-z0-9]{4,32}', new format '[a-z0-9]{5,64}', or group name format '[a-z0-9][a-z0-9_-]{3,63}'`
975
+ );
976
+ }
977
+ if (domain && !DOMAIN_RE.test(domain)) {
978
+ throw new ValidationError(
979
+ `Invalid ${paramName} '${String(raw)}': domain '${domain}' is not a valid domain`
980
+ );
981
+ }
982
+ }
983
+ function validateGroupAIDFormat(groupId, paramNameOrOptions = "group_aid", maybeOptions) {
984
+ const { paramName, localIssuer } = resolveGroupValidationArgs(
985
+ "group_aid",
986
+ paramNameOrOptions,
987
+ maybeOptions
988
+ );
989
+ if (groupId === null || groupId === void 0 || typeof groupId === "string" && groupId.trim() === "") {
990
+ throw new ValidationError(`${paramName} cannot be empty`);
991
+ }
992
+ const rawText = String(groupId).trim();
993
+ if (rawText.includes("//")) {
994
+ throw new ValidationError(
995
+ `Invalid ${paramName} '${String(groupId)}': empty path segment is not allowed`
996
+ );
997
+ }
998
+ const groupAid = convertToGroupAid(rawText, { localIssuer });
999
+ if (!groupAid) {
1000
+ throw new ValidationError(`${paramName} cannot be empty`);
1001
+ }
1002
+ validateGroupAidParts(groupId, groupAid, paramName);
1003
+ return groupAid;
1004
+ }
1005
+ function validateGroupIDFormat(groupId, paramNameOrOptions = "group_id", maybeOptions) {
1006
+ const { paramName, localIssuer } = resolveGroupValidationArgs(
1007
+ "group_id",
1008
+ paramNameOrOptions,
1009
+ maybeOptions
1010
+ );
1011
+ return validateGroupAIDFormat(groupId, paramName, { localIssuer });
931
1012
  }
932
1013
 
933
1014
  // src/discovery.ts
@@ -1456,7 +1537,7 @@ var _noopLog3 = { error: () => {
1456
1537
  }, info: () => {
1457
1538
  }, debug: () => {
1458
1539
  } };
1459
- var AID_NAME_RE = /^[a-z0-9_][a-z0-9_-]{3,63}$/;
1540
+ var AID_NAME_RE2 = /^[a-z0-9_][a-z0-9_-]{3,63}$/;
1460
1541
  function _gatewayHttpUrl(gatewayUrl, path) {
1461
1542
  try {
1462
1543
  const parsed = new URL(gatewayUrl);
@@ -1508,7 +1589,7 @@ var RegisterFlow = class _RegisterFlow {
1508
1589
  }
1509
1590
  static validateAidName(aid) {
1510
1591
  const name = aid.includes(".") ? aid.split(".")[0] : aid;
1511
- if (!AID_NAME_RE.test(name)) {
1592
+ if (!AID_NAME_RE2.test(name)) {
1512
1593
  throw new ValidationError(
1513
1594
  `Invalid AID name '${name}': must be 4-64 characters, only [a-z0-9_-], cannot start with '-'`
1514
1595
  );
@@ -1835,16 +1916,12 @@ var STORE_KEY_PAIRS = "key_pairs";
1835
1916
  var STORE_CERTS = "certs";
1836
1917
  var STORE_METADATA = "metadata";
1837
1918
  var STORE_INSTANCE_STATE = "instance_state";
1838
- var STORE_PREKEYS = "prekeys";
1839
- var STORE_GROUP_CURRENT = "group_current";
1840
- var STORE_GROUP_OLD_EPOCHS = "group_old_epochs";
1841
- var STORE_SESSIONS = "e2ee_sessions";
1842
1919
  var STORE_GROUP_STATE = "group_state";
1843
1920
  var STORE_AGENT_MD_CACHE = "agent_md_cache";
1844
1921
  var STORE_PENDING_IDENTITIES = "pending_identities";
1845
- var STRUCTURED_RECOVERY_RETENTION_MS = 7 * 24 * 3600 * 1e3;
1922
+ var STORE_PENDING_BINDS = "pending_binds";
1846
1923
  var DB_NAME = "aun-keystore";
1847
- var DB_VERSION = 7;
1924
+ var DB_VERSION = 8;
1848
1925
  function safeAid(aid) {
1849
1926
  return aid.replace(/[/\\:]/g, "_");
1850
1927
  }
@@ -1860,9 +1937,6 @@ function isRecord(value) {
1860
1937
  function toBufferSource2(bytes) {
1861
1938
  return bytes.slice().buffer;
1862
1939
  }
1863
- function sameJson(a, b) {
1864
- return JSON.stringify(a) === JSON.stringify(b);
1865
- }
1866
1940
  function randomHex(byteLength) {
1867
1941
  const bytes = crypto.getRandomValues(new Uint8Array(byteLength));
1868
1942
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
@@ -1898,36 +1972,6 @@ function instanceStateStoreKey(aid, deviceId, slotId = "") {
1898
1972
  const slotKey = slotIsolationKey(slotId) || "_singleton";
1899
1973
  return `${safeAid(aid)}|${encodePart(normalizedDevice)}|${encodePart(slotKey)}`;
1900
1974
  }
1901
- function prekeyPrefix(aid) {
1902
- return `${safeAid(aid)}|`;
1903
- }
1904
- function prekeyStoreKey(aid, prekeyId, deviceId = "") {
1905
- const normalizedDeviceId = String(deviceId ?? "").trim();
1906
- if (!normalizedDeviceId) {
1907
- return `${safeAid(aid)}|${encodePart(prekeyId)}`;
1908
- }
1909
- return `${safeAid(aid)}|${encodePart(normalizedDeviceId)}|${encodePart(prekeyId)}`;
1910
- }
1911
- function groupCurrentPrefix(aid) {
1912
- return `${safeAid(aid)}|`;
1913
- }
1914
- function groupCurrentStoreKey(aid, groupId) {
1915
- return `${safeAid(aid)}|${encodePart(groupId)}`;
1916
- }
1917
- function groupOldPrefix(aid, groupId) {
1918
- const base = `${safeAid(aid)}|`;
1919
- if (groupId === void 0) return base;
1920
- return `${base}${encodePart(groupId)}|`;
1921
- }
1922
- function groupOldStoreKey(aid, groupId, epoch) {
1923
- return `${safeAid(aid)}|${encodePart(groupId)}|${epoch}`;
1924
- }
1925
- function sessionPrefix(aid) {
1926
- return `${safeAid(aid)}|`;
1927
- }
1928
- function sessionStoreKey(aid, sessionId) {
1929
- return `${safeAid(aid)}|${encodePart(sessionId)}`;
1930
- }
1931
1975
  function seqTrackerPrefix(aid, deviceId, slotId) {
1932
1976
  const normalizedDevice = normalizeInstanceId(deviceId, "device_id");
1933
1977
  const slotKey = slotIsolationKey(slotId) || "_singleton";
@@ -1946,11 +1990,7 @@ function pendingIdentityPrefix(aid) {
1946
1990
  return `${safeAid(aid)}-`;
1947
1991
  }
1948
1992
  function stripStructuredFields(metadata) {
1949
- const plain = deepClone(metadata);
1950
- delete plain.e2ee_prekeys;
1951
- delete plain.group_secrets;
1952
- delete plain.e2ee_sessions;
1953
- return plain;
1993
+ return deepClone(metadata);
1954
1994
  }
1955
1995
  function defaultAgentMdCacheRecord(aid) {
1956
1996
  return {
@@ -2056,13 +2096,10 @@ function openDB() {
2056
2096
  STORE_CERTS,
2057
2097
  STORE_METADATA,
2058
2098
  STORE_INSTANCE_STATE,
2059
- STORE_PREKEYS,
2060
- STORE_GROUP_CURRENT,
2061
- STORE_GROUP_OLD_EPOCHS,
2062
- STORE_SESSIONS,
2063
2099
  STORE_GROUP_STATE,
2064
2100
  STORE_AGENT_MD_CACHE,
2065
- STORE_PENDING_IDENTITIES
2101
+ STORE_PENDING_IDENTITIES,
2102
+ STORE_PENDING_BINDS
2066
2103
  ]) {
2067
2104
  if (!db.objectStoreNames.contains(name)) db.createObjectStore(name);
2068
2105
  }
@@ -2671,12 +2708,90 @@ var _IndexedDBIdentityStore = class _IndexedDBIdentityStore {
2671
2708
  const plain = stripStructuredFields(metadata);
2672
2709
  await idbPut(STORE_METADATA, metadataStoreKey(aid), plain);
2673
2710
  }
2711
+ // ── Pending Group Bind (幂等暂存槽位) ────────────────────
2712
+ async savePendingGroupBind(groupId, keyPair) {
2713
+ const gid = groupId.trim();
2714
+ if (!gid) throw new Error("savePendingGroupBind requires non-empty group_id");
2715
+ const privateKeyPem = String(keyPair.private_key_pem || "").trim();
2716
+ const publicKeyDerB642 = String(keyPair.public_key_der_b64 || "").trim();
2717
+ if (!privateKeyPem || !publicKeyDerB642) {
2718
+ throw new Error("savePendingGroupBind requires private_key_pem and public_key_der_b64");
2719
+ }
2720
+ const curve = String(keyPair.curve || "P-256").trim() || "P-256";
2721
+ const record = {
2722
+ public_key_der_b64: publicKeyDerB642,
2723
+ curve
2724
+ };
2725
+ if (hasEncryptionSeed(this._encryptionSeed)) {
2726
+ record._encrypted_pk = await encryptPEM(privateKeyPem, this._encryptionSeed);
2727
+ } else {
2728
+ record.private_key_pem = privateKeyPem;
2729
+ }
2730
+ await idbPut("pending_binds", gid, record);
2731
+ }
2732
+ async loadPendingGroupBind(groupId) {
2733
+ const gid = groupId.trim();
2734
+ if (!gid) return null;
2735
+ try {
2736
+ const data = await idbGet("pending_binds", gid);
2737
+ if (!isRecord(data)) return null;
2738
+ const result = deepClone(data);
2739
+ if (isRecord(result._encrypted_pk) && hasEncryptionSeed(this._encryptionSeed)) {
2740
+ try {
2741
+ const envelope = result._encrypted_pk;
2742
+ result.private_key_pem = await decryptPEM(envelope, this._encryptionSeed);
2743
+ delete result._encrypted_pk;
2744
+ } catch (e) {
2745
+ this._log.warn(`loadPendingGroupBind decrypt failed for ${gid}: ${e instanceof Error ? e.message : String(e)}`);
2746
+ return null;
2747
+ }
2748
+ }
2749
+ return {
2750
+ public_key_der_b64: String(result.public_key_der_b64 || ""),
2751
+ private_key_pem: String(result.private_key_pem || ""),
2752
+ curve: String(result.curve || "P-256")
2753
+ };
2754
+ } catch (e) {
2755
+ this._log.warn(`loadPendingGroupBind failed for ${gid}: ${e instanceof Error ? e.message : String(e)}`);
2756
+ return null;
2757
+ }
2758
+ }
2759
+ async clearPendingGroupBind(groupId) {
2760
+ const gid = groupId.trim();
2761
+ if (!gid) return;
2762
+ try {
2763
+ await idbDelete("pending_binds", gid);
2764
+ } catch (e) {
2765
+ this._log.warn(`clearPendingGroupBind failed for ${gid}: ${e instanceof Error ? e.message : String(e)}`);
2766
+ }
2767
+ }
2674
2768
  };
2675
2769
  __publicField(_IndexedDBIdentityStore, "_aidTails", /* @__PURE__ */ new Map());
2676
2770
  var IndexedDBIdentityStore = _IndexedDBIdentityStore;
2677
2771
 
2678
2772
  // src/keystore/indexeddb-token-store.ts
2679
2773
  init_crypto();
2774
+ function groupLookupCandidates(groupId, localIssuer = "") {
2775
+ const raw = String(groupId ?? "").trim();
2776
+ if (!raw) return [];
2777
+ const normalized = convertToGroupAid(raw, { localIssuer });
2778
+ const candidates = [];
2779
+ const add = (value) => {
2780
+ const trimmed = String(value ?? "").trim();
2781
+ if (trimmed && !candidates.includes(trimmed)) candidates.push(trimmed);
2782
+ };
2783
+ add(raw);
2784
+ add(normalized);
2785
+ const dotIdx = normalized.indexOf(".");
2786
+ if (dotIdx > 0 && dotIdx < normalized.length - 1) {
2787
+ const base = normalized.slice(0, dotIdx);
2788
+ const issuer = normalized.slice(dotIdx + 1);
2789
+ add(`group.${issuer}/${base}`);
2790
+ add(`${base}@${issuer}`);
2791
+ if (localIssuer && issuer === localIssuer.replace(/^\.+|\.+$/g, "").toLowerCase()) add(base);
2792
+ }
2793
+ return candidates;
2794
+ }
2680
2795
  var _IndexedDBTokenStore = class _IndexedDBTokenStore {
2681
2796
  constructor() {
2682
2797
  __publicField(this, "_log", _noopLog4);
@@ -2774,187 +2889,6 @@ var _IndexedDBTokenStore = class _IndexedDBTokenStore {
2774
2889
  return deepClone(updated);
2775
2890
  });
2776
2891
  }
2777
- // ── 结构化 prekeys ───────────────────────────────────
2778
- async loadE2EEPrekeys(aid, deviceId) {
2779
- return this._withAidLock(aid, async () => {
2780
- await this._migrateLegacyStructuredStateUnlocked(aid);
2781
- return this._loadPrekeysUnlocked(aid, String(deviceId ?? "").trim());
2782
- });
2783
- }
2784
- /**
2785
- * 按 prekey_id 单点查询本地 prekey(O(log N) IndexedDB primary-key 查找)。
2786
- *
2787
- * 解密入站消息时,envelope 里只带 prekey_id,没有 device_id。优先用 IndexedDB
2788
- * 主键直查;命中则 O(log N),未命中再回退到前缀扫描,保证跨 device_id 也能找到。
2789
- */
2790
- async loadE2EEPrekeyById(aid, prekeyId) {
2791
- return this._withAidLock(aid, async () => {
2792
- await this._migrateLegacyStructuredStateUnlocked(aid);
2793
- return this._loadPrekeyByIdUnlocked(aid, prekeyId);
2794
- });
2795
- }
2796
- async saveE2EEPrekey(aid, prekeyId, prekeyData, deviceId) {
2797
- await this._withAidLock(aid, async () => {
2798
- await this._migrateLegacyStructuredStateUnlocked(aid);
2799
- const record = deepClone(prekeyData);
2800
- record.prekey_id = prekeyId;
2801
- const normalizedDeviceId = String(deviceId ?? "").trim();
2802
- if (normalizedDeviceId) {
2803
- record.device_id = normalizedDeviceId;
2804
- } else {
2805
- delete record.device_id;
2806
- }
2807
- const targetKey = prekeyStoreKey(aid, prekeyId, normalizedDeviceId);
2808
- for (const item of await idbGetAllByPrefix(STORE_PREKEYS, prekeyPrefix(aid))) {
2809
- if (!isRecord(item.value)) continue;
2810
- const existingPrekeyId = typeof item.value.prekey_id === "string" ? item.value.prekey_id : "";
2811
- const existingDeviceId = String(item.value.device_id ?? "").trim();
2812
- if (existingPrekeyId === prekeyId && existingDeviceId === normalizedDeviceId && item.key !== targetKey) {
2813
- await idbDelete(STORE_PREKEYS, item.key);
2814
- }
2815
- }
2816
- await idbPut(STORE_PREKEYS, targetKey, record);
2817
- });
2818
- }
2819
- async cleanupE2EEPrekeys(aid, cutoffMs, keepLatest = 7, deviceId) {
2820
- return this._withAidLock(aid, async () => {
2821
- await this._migrateLegacyStructuredStateUnlocked(aid);
2822
- const normalizedDeviceId = String(deviceId ?? "").trim();
2823
- const prekeys = await this._loadPrekeysUnlocked(aid, normalizedDeviceId);
2824
- const retainedIds = new Set(
2825
- Object.entries(prekeys).sort((left, right) => {
2826
- const leftMarker = Number(left[1].created_at ?? left[1].updated_at ?? left[1].expires_at ?? 0);
2827
- const rightMarker = Number(right[1].created_at ?? right[1].updated_at ?? right[1].expires_at ?? 0);
2828
- if (rightMarker !== leftMarker) return rightMarker - leftMarker;
2829
- return right[0].localeCompare(left[0]);
2830
- }).slice(0, keepLatest).map(([prekeyId]) => prekeyId)
2831
- );
2832
- const removed = Object.entries(prekeys).filter(
2833
- ([prekeyId, data]) => Number(data.created_at ?? data.updated_at ?? data.expires_at ?? 0) < cutoffMs && !retainedIds.has(prekeyId)
2834
- ).map(([prekeyId]) => prekeyId);
2835
- const removedIds = new Set(removed);
2836
- for (const item of await idbGetAllByPrefix(STORE_PREKEYS, prekeyPrefix(aid))) {
2837
- if (!isRecord(item.value)) continue;
2838
- const recordDeviceId = String(item.value.device_id ?? "").trim();
2839
- if (recordDeviceId !== normalizedDeviceId) continue;
2840
- const prekeyId = typeof item.value.prekey_id === "string" ? item.value.prekey_id : "";
2841
- if (removedIds.has(prekeyId)) await idbDelete(STORE_PREKEYS, item.key);
2842
- }
2843
- return removed;
2844
- });
2845
- }
2846
- // ── 结构化群组密钥 ─────────────────────────────────
2847
- async listGroupSecretIds(aid) {
2848
- return this._withAidLock(aid, async () => {
2849
- await this._migrateLegacyStructuredStateUnlocked(aid);
2850
- const ids = /* @__PURE__ */ new Set();
2851
- for (const item of await idbGetAllByPrefix(STORE_GROUP_CURRENT, groupCurrentPrefix(aid))) {
2852
- const [, encodedGroupId] = item.key.split("|");
2853
- if (encodedGroupId) ids.add(decodeURIComponent(encodedGroupId));
2854
- }
2855
- for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid))) {
2856
- const [, encodedGroupId] = item.key.split("|");
2857
- if (encodedGroupId) ids.add(decodeURIComponent(encodedGroupId));
2858
- }
2859
- return [...ids].sort();
2860
- });
2861
- }
2862
- async cleanupGroupOldEpochsState(aid, groupId, cutoffMs) {
2863
- return this._withAidLock(aid, async () => {
2864
- await this._migrateLegacyStructuredStateUnlocked(aid);
2865
- let removed = 0;
2866
- for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
2867
- if (!isRecord(item.value)) continue;
2868
- if (Number(item.value.updated_at ?? 0) <= cutoffMs) {
2869
- await idbDelete(STORE_GROUP_OLD_EPOCHS, item.key);
2870
- removed += 1;
2871
- }
2872
- }
2873
- return removed;
2874
- });
2875
- }
2876
- async loadGroupSecretEpoch(aid, groupId, epoch) {
2877
- return this._withAidLock(aid, async () => {
2878
- await this._migrateLegacyStructuredStateUnlocked(aid);
2879
- const current = await idbGet(STORE_GROUP_CURRENT, groupCurrentStoreKey(aid, groupId));
2880
- if (isRecord(current)) {
2881
- const record2 = deepClone(current);
2882
- delete record2.group_id;
2883
- if (epoch === void 0 || epoch === null || Number(record2.epoch ?? 0) === Number(epoch)) {
2884
- return record2;
2885
- }
2886
- } else if (epoch === void 0 || epoch === null) {
2887
- return null;
2888
- }
2889
- const old = await idbGet(STORE_GROUP_OLD_EPOCHS, groupOldStoreKey(aid, groupId, Number(epoch)));
2890
- if (!isRecord(old)) return null;
2891
- const record = deepClone(old);
2892
- delete record.group_id;
2893
- return record;
2894
- });
2895
- }
2896
- async loadGroupSecretEpochs(aid, groupId) {
2897
- return this._withAidLock(aid, async () => {
2898
- await this._migrateLegacyStructuredStateUnlocked(aid);
2899
- const result = [];
2900
- const current = await idbGet(STORE_GROUP_CURRENT, groupCurrentStoreKey(aid, groupId));
2901
- if (isRecord(current)) {
2902
- const record = deepClone(current);
2903
- delete record.group_id;
2904
- result.push(record);
2905
- }
2906
- for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
2907
- if (!isRecord(item.value)) continue;
2908
- const record = deepClone(item.value);
2909
- delete record.group_id;
2910
- result.push(record);
2911
- }
2912
- return result;
2913
- });
2914
- }
2915
- async storeGroupSecretTransition(aid, groupId, opts) {
2916
- return this._withAidLock(aid, async () => {
2917
- await this._migrateLegacyStructuredStateUnlocked(aid);
2918
- return this._storeGroupSecretTransitionUnlocked(aid, groupId, opts);
2919
- });
2920
- }
2921
- async storeGroupSecretEpoch(aid, groupId, opts) {
2922
- return this._withAidLock(aid, async () => {
2923
- await this._migrateLegacyStructuredStateUnlocked(aid);
2924
- return this._storeGroupSecretEpochUnlocked(aid, groupId, opts);
2925
- });
2926
- }
2927
- async discardPendingGroupSecretState(aid, groupId, epoch, rotationId) {
2928
- return this._withAidLock(aid, async () => {
2929
- const rid = String(rotationId ?? "").trim();
2930
- if (!rid) return false;
2931
- await this._migrateLegacyStructuredStateUnlocked(aid);
2932
- return this._discardPendingGroupSecretUnlocked(aid, groupId, epoch, rid);
2933
- });
2934
- }
2935
- async deleteGroupSecretState(aid, groupId) {
2936
- await this._withAidLock(aid, async () => {
2937
- await idbDelete(STORE_GROUP_CURRENT, groupCurrentStoreKey(aid, groupId));
2938
- for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
2939
- await idbDelete(STORE_GROUP_OLD_EPOCHS, item.key);
2940
- }
2941
- });
2942
- }
2943
- // ── E2EE Sessions ─────────────────────────────────────
2944
- async loadE2EESessions(aid) {
2945
- return this._withAidLock(aid, async () => {
2946
- await this._migrateLegacySessionsUnlocked(aid);
2947
- return this._loadSessionsUnlocked(aid);
2948
- });
2949
- }
2950
- async saveE2EESession(aid, sessionId, data) {
2951
- await this._withAidLock(aid, async () => {
2952
- await this._migrateLegacySessionsUnlocked(aid);
2953
- const record = deepClone(data);
2954
- record.session_id = sessionId;
2955
- await idbPut(STORE_SESSIONS, sessionStoreKey(aid, sessionId), record);
2956
- });
2957
- }
2958
2892
  /**
2959
2893
  * 读取单个 metadata KV(如 gateway_url 等跨进程缓存项)。
2960
2894
  * 不存在时返回空字符串,与 Python keystore 的 get_metadata 语义保持一致。
@@ -3067,18 +3001,23 @@ var _IndexedDBTokenStore = class _IndexedDBTokenStore {
3067
3001
  });
3068
3002
  }
3069
3003
  async loadGroupState(groupId) {
3070
- const key = encodePart(groupId);
3071
- const data = await idbGet(STORE_GROUP_STATE, key);
3072
- if (!data || typeof data.state_version !== "number") return null;
3073
- return {
3074
- group_id: groupId,
3075
- state_version: data.state_version,
3076
- state_hash: data.state_hash ?? "",
3077
- key_epoch: data.key_epoch ?? 0,
3078
- membership_json: data.membership_json ?? "[]",
3079
- policy_json: data.policy_json ?? "{}",
3080
- updated_at: data.updated_at ?? 0
3081
- };
3004
+ for (const candidate of groupLookupCandidates(groupId)) {
3005
+ const key = encodePart(candidate);
3006
+ const data = await idbGet(STORE_GROUP_STATE, key);
3007
+ if (!data || typeof data.state_version !== "number") continue;
3008
+ const storedGroupId = typeof data.group_id === "string" ? data.group_id : candidate;
3009
+ return {
3010
+ group_id: storedGroupId,
3011
+ group_aid: convertToGroupAid(storedGroupId),
3012
+ state_version: data.state_version,
3013
+ state_hash: data.state_hash ?? "",
3014
+ key_epoch: data.key_epoch ?? 0,
3015
+ membership_json: data.membership_json ?? "[]",
3016
+ policy_json: data.policy_json ?? "{}",
3017
+ updated_at: data.updated_at ?? 0
3018
+ };
3019
+ }
3020
+ return null;
3082
3021
  }
3083
3022
  /** 计算 PEM 证书的 SHA-256 指纹(hex,无冒号) */
3084
3023
  async _pemFingerprint(pem) {
@@ -3144,515 +3083,6 @@ var _IndexedDBTokenStore = class _IndexedDBTokenStore {
3144
3083
  const plain = stripStructuredFields(metadata);
3145
3084
  await idbPut(STORE_METADATA, metadataStoreKey(aid), plain);
3146
3085
  }
3147
- async _migrateLegacyStructuredStateUnlocked(aid) {
3148
- const metadataOnly = await this._loadMetadataOnlyUnlocked(aid) ?? {};
3149
- const hasLegacyPrekeys = isRecord(metadataOnly.e2ee_prekeys);
3150
- const hasLegacyGroups = isRecord(metadataOnly.group_secrets);
3151
- const hasLegacySessions = Array.isArray(metadataOnly.e2ee_sessions) && metadataOnly.e2ee_sessions.length > 0;
3152
- if (!hasLegacyPrekeys && !hasLegacyGroups && !hasLegacySessions) {
3153
- return metadataOnly;
3154
- }
3155
- const cleaned = deepClone(metadataOnly);
3156
- if (hasLegacyPrekeys) {
3157
- const legacyPrekeys = metadataOnly.e2ee_prekeys;
3158
- const structuredPrekeys = await this._loadPrekeysUnlocked(aid);
3159
- let changed = false;
3160
- if (isRecord(legacyPrekeys)) {
3161
- for (const [prekeyId, rawData] of Object.entries(legacyPrekeys)) {
3162
- if (!isRecord(rawData) || structuredPrekeys[prekeyId]) continue;
3163
- if (!this._isPrekeyRecoverable(rawData)) continue;
3164
- structuredPrekeys[prekeyId] = deepClone(rawData);
3165
- changed = true;
3166
- }
3167
- }
3168
- if (changed) {
3169
- await this._replacePrekeysUnlocked(aid, structuredPrekeys);
3170
- }
3171
- delete cleaned.e2ee_prekeys;
3172
- }
3173
- if (hasLegacyGroups) {
3174
- const legacyGroups = metadataOnly.group_secrets;
3175
- const structuredGroups = await this._loadGroupEntriesUnlocked(aid);
3176
- let changed = false;
3177
- if (isRecord(legacyGroups)) {
3178
- for (const [groupId, rawEntry] of Object.entries(legacyGroups)) {
3179
- if (!isRecord(rawEntry)) continue;
3180
- const merged = this._mergeGroupEntryFromLegacy(structuredGroups[groupId], rawEntry);
3181
- if (!sameJson(structuredGroups[groupId] ?? null, merged)) {
3182
- if (Object.keys(merged).length > 0) {
3183
- structuredGroups[groupId] = merged;
3184
- changed = true;
3185
- }
3186
- }
3187
- }
3188
- }
3189
- if (changed) {
3190
- await this._replaceGroupEntriesUnlocked(aid, structuredGroups);
3191
- }
3192
- delete cleaned.group_secrets;
3193
- }
3194
- if (hasLegacySessions) {
3195
- const legacySessions = metadataOnly.e2ee_sessions;
3196
- for (const session of legacySessions) {
3197
- const sid = typeof session.session_id === "string" ? session.session_id : "";
3198
- if (!sid) continue;
3199
- const record = deepClone(session);
3200
- record.session_id = sid;
3201
- await idbPut(STORE_SESSIONS, sessionStoreKey(aid, sid), record);
3202
- }
3203
- delete cleaned.e2ee_sessions;
3204
- }
3205
- await this._saveMetadataOnlyUnlocked(aid, cleaned);
3206
- return cleaned;
3207
- }
3208
- async _loadPrekeysUnlocked(aid, deviceId = "") {
3209
- const items = await idbGetAllByPrefix(STORE_PREKEYS, prekeyPrefix(aid));
3210
- const prefix = prekeyPrefix(aid);
3211
- const result = {};
3212
- const normalizedDeviceId = String(deviceId ?? "").trim();
3213
- for (const item of items) {
3214
- if (!item.key.startsWith(prefix) || !isRecord(item.value)) continue;
3215
- const recordDeviceId = String(item.value.device_id ?? "").trim();
3216
- if (recordDeviceId !== normalizedDeviceId) continue;
3217
- const prekeyId = typeof item.value.prekey_id === "string" ? item.value.prekey_id : item.key.slice(prefix.length);
3218
- const record = deepClone(item.value);
3219
- delete record.prekey_id;
3220
- delete record.device_id;
3221
- const canonicalKey = prekeyStoreKey(aid, prekeyId, normalizedDeviceId);
3222
- if (!(prekeyId in result) || item.key === canonicalKey) {
3223
- result[prekeyId] = record;
3224
- }
3225
- }
3226
- return result;
3227
- }
3228
- async _loadPrekeyByIdUnlocked(aid, prekeyId) {
3229
- if (!prekeyId) return null;
3230
- const directKey = prekeyStoreKey(aid, prekeyId, "");
3231
- const direct = await idbGet(STORE_PREKEYS, directKey);
3232
- if (isRecord(direct)) {
3233
- const record = deepClone(direct);
3234
- delete record.prekey_id;
3235
- delete record.device_id;
3236
- return record;
3237
- }
3238
- for (const item of await idbGetAllByPrefix(STORE_PREKEYS, prekeyPrefix(aid))) {
3239
- if (!isRecord(item.value)) continue;
3240
- const itemPrekeyId = typeof item.value.prekey_id === "string" ? item.value.prekey_id : "";
3241
- if (itemPrekeyId !== prekeyId) continue;
3242
- const record = deepClone(item.value);
3243
- delete record.prekey_id;
3244
- delete record.device_id;
3245
- return record;
3246
- }
3247
- return null;
3248
- }
3249
- async _replacePrekeysUnlocked(aid, prekeys, deviceId = "") {
3250
- const desired = new Set(Object.keys(prekeys));
3251
- const normalizedDeviceId = String(deviceId ?? "").trim();
3252
- for (const item of await idbGetAllByPrefix(STORE_PREKEYS, prekeyPrefix(aid))) {
3253
- if (!isRecord(item.value)) continue;
3254
- const recordDeviceId = String(item.value.device_id ?? "").trim();
3255
- if (recordDeviceId !== normalizedDeviceId) continue;
3256
- const prekeyId = typeof item.value.prekey_id === "string" ? item.value.prekey_id : "";
3257
- const canonicalKey = prekeyStoreKey(aid, prekeyId, normalizedDeviceId);
3258
- if (!desired.has(prekeyId) || item.key !== canonicalKey) {
3259
- await idbDelete(STORE_PREKEYS, item.key);
3260
- }
3261
- }
3262
- for (const [prekeyId, prekeyData] of Object.entries(prekeys)) {
3263
- const record = deepClone(prekeyData);
3264
- if (normalizedDeviceId) {
3265
- record.device_id = normalizedDeviceId;
3266
- } else {
3267
- delete record.device_id;
3268
- }
3269
- await idbPut(STORE_PREKEYS, prekeyStoreKey(aid, prekeyId, normalizedDeviceId), { ...record, prekey_id: prekeyId });
3270
- }
3271
- }
3272
- async _loadGroupEntriesUnlocked(aid) {
3273
- const result = {};
3274
- for (const item of await idbGetAllByPrefix(STORE_GROUP_CURRENT, groupCurrentPrefix(aid))) {
3275
- if (!isRecord(item.value)) continue;
3276
- const groupId = typeof item.value.group_id === "string" ? item.value.group_id : item.key.slice(groupCurrentPrefix(aid).length);
3277
- const record = deepClone(item.value);
3278
- delete record.group_id;
3279
- result[groupId] = record;
3280
- }
3281
- for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid))) {
3282
- if (!isRecord(item.value)) continue;
3283
- const groupId = typeof item.value.group_id === "string" ? item.value.group_id : "";
3284
- if (!groupId) continue;
3285
- const record = deepClone(item.value);
3286
- delete record.group_id;
3287
- const entry = result[groupId] ?? {};
3288
- const oldEpochs = Array.isArray(entry.old_epochs) ? entry.old_epochs : [];
3289
- oldEpochs.push(record);
3290
- oldEpochs.sort((a, b) => Number(a.epoch ?? 0) - Number(b.epoch ?? 0));
3291
- entry.old_epochs = oldEpochs;
3292
- result[groupId] = entry;
3293
- }
3294
- return result;
3295
- }
3296
- async _replaceGroupEntriesUnlocked(aid, groups) {
3297
- const desiredGroups = new Set(Object.keys(groups));
3298
- for (const item of await idbGetAllByPrefix(STORE_GROUP_CURRENT, groupCurrentPrefix(aid))) {
3299
- const groupId = isRecord(item.value) && typeof item.value.group_id === "string" ? item.value.group_id : item.key.slice(groupCurrentPrefix(aid).length);
3300
- if (!desiredGroups.has(groupId)) await idbDelete(STORE_GROUP_CURRENT, item.key);
3301
- }
3302
- for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid))) {
3303
- if (!isRecord(item.value)) continue;
3304
- const groupId = typeof item.value.group_id === "string" ? item.value.group_id : "";
3305
- if (!groupId || desiredGroups.has(groupId)) continue;
3306
- await idbDelete(STORE_GROUP_OLD_EPOCHS, item.key);
3307
- }
3308
- for (const [groupId, rawEntry] of Object.entries(groups)) {
3309
- if (!isRecord(rawEntry)) continue;
3310
- await this._saveSingleGroupEntryUnlocked(aid, groupId, rawEntry);
3311
- }
3312
- }
3313
- async _saveSingleGroupEntryUnlocked(aid, groupId, entry) {
3314
- for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
3315
- if (!isRecord(item.value)) continue;
3316
- if (typeof item.value.group_id === "string" && item.value.group_id === groupId) {
3317
- await idbDelete(STORE_GROUP_OLD_EPOCHS, item.key);
3318
- }
3319
- }
3320
- const current = deepClone(entry);
3321
- const oldEpochs = Array.isArray(current.old_epochs) ? current.old_epochs : [];
3322
- delete current.old_epochs;
3323
- if (typeof current.epoch === "number") {
3324
- await idbPut(STORE_GROUP_CURRENT, groupCurrentStoreKey(aid, groupId), { ...current, group_id: groupId });
3325
- } else {
3326
- await idbDelete(STORE_GROUP_CURRENT, groupCurrentStoreKey(aid, groupId));
3327
- }
3328
- for (const oldEpoch of oldEpochs) {
3329
- if (!isRecord(oldEpoch) || typeof oldEpoch.epoch !== "number") continue;
3330
- await idbPut(STORE_GROUP_OLD_EPOCHS, groupOldStoreKey(aid, groupId, oldEpoch.epoch), {
3331
- ...deepClone(oldEpoch),
3332
- group_id: groupId
3333
- });
3334
- }
3335
- }
3336
- async _storeGroupSecretTransitionUnlocked(aid, groupId, opts) {
3337
- const currentKey = groupCurrentStoreKey(aid, groupId);
3338
- const currentRaw = await idbGet(STORE_GROUP_CURRENT, currentKey);
3339
- const now = Date.now();
3340
- const epoch = Number(opts.epoch);
3341
- const members = [...opts.memberAids ?? []].map((item) => String(item)).sort();
3342
- const pendingRotationId = String(opts.pendingRotationId ?? "").trim();
3343
- if (isRecord(currentRaw)) {
3344
- const current = deepClone(currentRaw);
3345
- delete current.group_id;
3346
- const localEpoch = Number(current.epoch ?? 0);
3347
- if (epoch < localEpoch) return false;
3348
- if (epoch === localEpoch && typeof current.secret === "string") {
3349
- if (current.secret !== opts.secret) {
3350
- if (String(current.pending_rotation_id ?? "").trim()) {
3351
- await idbPut(STORE_GROUP_CURRENT, currentKey, {
3352
- ...this._buildGroupCurrentRecord(groupId, opts, members, now)
3353
- });
3354
- return true;
3355
- }
3356
- return false;
3357
- }
3358
- const updated = { ...current };
3359
- let changed = false;
3360
- const oldMembers = Array.isArray(updated.member_aids) ? updated.member_aids.map(String).sort() : [];
3361
- if (members.length > 0 && JSON.stringify(oldMembers) !== JSON.stringify(members)) {
3362
- updated.member_aids = members;
3363
- updated.commitment = opts.commitment;
3364
- updated.updated_at = now;
3365
- changed = true;
3366
- }
3367
- if (opts.epochChain !== void 0 && updated.epoch_chain !== opts.epochChain) {
3368
- updated.epoch_chain = opts.epochChain;
3369
- updated.updated_at = now;
3370
- changed = true;
3371
- }
3372
- if (opts.epochChainUnverified === true) {
3373
- if (updated.epoch_chain_unverified !== true) {
3374
- updated.epoch_chain_unverified = true;
3375
- changed = true;
3376
- }
3377
- if (opts.epochChainUnverifiedReason && updated.epoch_chain_unverified_reason !== opts.epochChainUnverifiedReason) {
3378
- updated.epoch_chain_unverified_reason = opts.epochChainUnverifiedReason;
3379
- changed = true;
3380
- }
3381
- } else if (opts.epochChainUnverified === false) {
3382
- if ("epoch_chain_unverified" in updated || "epoch_chain_unverified_reason" in updated) {
3383
- delete updated.epoch_chain_unverified;
3384
- delete updated.epoch_chain_unverified_reason;
3385
- changed = true;
3386
- }
3387
- }
3388
- if (pendingRotationId && updated.pending_rotation_id !== pendingRotationId) {
3389
- updated.pending_rotation_id = pendingRotationId;
3390
- updated.pending_created_at = now;
3391
- changed = true;
3392
- }
3393
- if (!pendingRotationId && updated.pending_rotation_id) {
3394
- delete updated.pending_rotation_id;
3395
- delete updated.pending_created_at;
3396
- changed = true;
3397
- }
3398
- if (changed) {
3399
- await idbPut(STORE_GROUP_CURRENT, currentKey, { ...updated, group_id: groupId });
3400
- }
3401
- return true;
3402
- }
3403
- if (localEpoch !== epoch) {
3404
- const oldEntry = { ...current };
3405
- oldEntry.expires_at = Number(current.updated_at ?? now) + opts.oldEpochRetentionMs;
3406
- await idbPut(STORE_GROUP_OLD_EPOCHS, groupOldStoreKey(aid, groupId, localEpoch), {
3407
- ...oldEntry,
3408
- group_id: groupId
3409
- });
3410
- } else {
3411
- const merged = { ...current, ...this._buildGroupCurrentRecord(groupId, opts, members, now) };
3412
- if (!opts.epochChain && current.epoch_chain) {
3413
- merged.epoch_chain = current.epoch_chain;
3414
- }
3415
- if (current.pending_rotation_id && !opts.pendingRotationId) {
3416
- merged.pending_rotation_id = current.pending_rotation_id;
3417
- if (current.pending_created_at) merged.pending_created_at = current.pending_created_at;
3418
- }
3419
- await idbPut(STORE_GROUP_CURRENT, currentKey, merged);
3420
- return true;
3421
- }
3422
- }
3423
- await idbPut(STORE_GROUP_CURRENT, currentKey, {
3424
- ...this._buildGroupCurrentRecord(groupId, opts, members, now)
3425
- });
3426
- return true;
3427
- }
3428
- async _storeGroupSecretEpochUnlocked(aid, groupId, opts) {
3429
- const now = Date.now();
3430
- const epoch = Number(opts.epoch);
3431
- const members = [...opts.memberAids ?? []].map((item) => String(item)).sort();
3432
- const pendingRotationId = String(opts.pendingRotationId ?? "").trim();
3433
- const currentKey = groupCurrentStoreKey(aid, groupId);
3434
- const currentRaw = await idbGet(STORE_GROUP_CURRENT, currentKey);
3435
- const newRecord = this._buildGroupCurrentRecord(groupId, opts, members, now);
3436
- if (!isRecord(currentRaw)) {
3437
- await idbPut(STORE_GROUP_CURRENT, currentKey, newRecord);
3438
- return true;
3439
- }
3440
- const current = deepClone(currentRaw);
3441
- delete current.group_id;
3442
- const localEpoch = Number(current.epoch ?? 0);
3443
- if (epoch > localEpoch) {
3444
- const expiresAt = Date.now() + (opts.oldEpochRetentionMs ?? 6048e5);
3445
- const oldRecord = { ...current, group_id: groupId, expires_at: expiresAt };
3446
- const oldKey2 = groupOldStoreKey(aid, groupId, localEpoch);
3447
- await idbPut(STORE_GROUP_OLD_EPOCHS, oldKey2, oldRecord);
3448
- await idbPut(STORE_GROUP_CURRENT, currentKey, newRecord);
3449
- return true;
3450
- }
3451
- if (epoch === localEpoch) {
3452
- if (typeof current.secret === "string" && current.secret !== opts.secret) {
3453
- if (!String(current.pending_rotation_id ?? "").trim()) return false;
3454
- await idbPut(STORE_GROUP_CURRENT, currentKey, newRecord);
3455
- return true;
3456
- }
3457
- const updated = { ...current };
3458
- let changed = false;
3459
- const oldMembers = Array.isArray(updated.member_aids) ? updated.member_aids.map(String).sort() : [];
3460
- if (members.length > 0 && JSON.stringify(oldMembers) !== JSON.stringify(members)) {
3461
- updated.member_aids = members;
3462
- updated.commitment = opts.commitment;
3463
- updated.updated_at = now;
3464
- changed = true;
3465
- }
3466
- if (opts.epochChain !== void 0 && updated.epoch_chain !== opts.epochChain) {
3467
- updated.epoch_chain = opts.epochChain;
3468
- updated.updated_at = now;
3469
- changed = true;
3470
- }
3471
- if (opts.epochChainUnverified === true) {
3472
- if (updated.epoch_chain_unverified !== true) {
3473
- updated.epoch_chain_unverified = true;
3474
- changed = true;
3475
- }
3476
- if (opts.epochChainUnverifiedReason && updated.epoch_chain_unverified_reason !== opts.epochChainUnverifiedReason) {
3477
- updated.epoch_chain_unverified_reason = opts.epochChainUnverifiedReason;
3478
- changed = true;
3479
- }
3480
- } else if (opts.epochChainUnverified === false) {
3481
- if ("epoch_chain_unverified" in updated || "epoch_chain_unverified_reason" in updated) {
3482
- delete updated.epoch_chain_unverified;
3483
- delete updated.epoch_chain_unverified_reason;
3484
- changed = true;
3485
- }
3486
- }
3487
- if (pendingRotationId && updated.pending_rotation_id !== pendingRotationId) {
3488
- updated.pending_rotation_id = pendingRotationId;
3489
- updated.pending_created_at = now;
3490
- changed = true;
3491
- }
3492
- if (!pendingRotationId && updated.pending_rotation_id) {
3493
- delete updated.pending_rotation_id;
3494
- delete updated.pending_created_at;
3495
- changed = true;
3496
- }
3497
- if (changed) await idbPut(STORE_GROUP_CURRENT, currentKey, { ...updated, group_id: groupId });
3498
- return true;
3499
- }
3500
- const oldKey = groupOldStoreKey(aid, groupId, epoch);
3501
- const oldRaw = await idbGet(STORE_GROUP_OLD_EPOCHS, oldKey);
3502
- if (isRecord(oldRaw)) {
3503
- const old = deepClone(oldRaw);
3504
- if (typeof old.secret === "string" && old.secret !== opts.secret) return false;
3505
- }
3506
- await idbPut(STORE_GROUP_OLD_EPOCHS, oldKey, {
3507
- ...newRecord,
3508
- epoch,
3509
- expires_at: now + opts.oldEpochRetentionMs
3510
- });
3511
- return true;
3512
- }
3513
- async _discardPendingGroupSecretUnlocked(aid, groupId, epoch, rotationId) {
3514
- const currentKey = groupCurrentStoreKey(aid, groupId);
3515
- const currentRaw = await idbGet(STORE_GROUP_CURRENT, currentKey);
3516
- if (!isRecord(currentRaw)) return false;
3517
- const current = deepClone(currentRaw);
3518
- if (Number(current.epoch ?? 0) !== Number(epoch)) return false;
3519
- if (String(current.pending_rotation_id ?? "").trim() !== rotationId) return false;
3520
- let restored = null;
3521
- let restoredKey = "";
3522
- for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
3523
- if (!isRecord(item.value)) continue;
3524
- const old = deepClone(item.value);
3525
- const oldEpoch = Number(old.epoch ?? 0);
3526
- if (oldEpoch >= Number(epoch) || !old.secret) continue;
3527
- if (!restored || oldEpoch > Number(restored.epoch ?? 0)) {
3528
- restored = old;
3529
- restoredKey = item.key;
3530
- }
3531
- }
3532
- if (!restored) {
3533
- await idbDelete(STORE_GROUP_CURRENT, currentKey);
3534
- for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
3535
- await idbDelete(STORE_GROUP_OLD_EPOCHS, item.key);
3536
- }
3537
- return true;
3538
- }
3539
- delete restored.group_id;
3540
- delete restored.expires_at;
3541
- await idbPut(STORE_GROUP_CURRENT, currentKey, { ...restored, group_id: groupId, updated_at: Date.now() });
3542
- if (restoredKey) await idbDelete(STORE_GROUP_OLD_EPOCHS, restoredKey);
3543
- return true;
3544
- }
3545
- _buildGroupCurrentRecord(groupId, opts, memberAids, now) {
3546
- const record = {
3547
- group_id: groupId,
3548
- epoch: opts.epoch,
3549
- secret: opts.secret,
3550
- commitment: opts.commitment,
3551
- member_aids: memberAids,
3552
- updated_at: now
3553
- };
3554
- if (opts.epochChain !== void 0) record.epoch_chain = opts.epochChain;
3555
- const pendingRotationId = String(opts.pendingRotationId ?? "").trim();
3556
- if (pendingRotationId) {
3557
- record.pending_rotation_id = pendingRotationId;
3558
- record.pending_created_at = now;
3559
- }
3560
- if (opts.epochChainUnverified === true) {
3561
- record.epoch_chain_unverified = true;
3562
- if (opts.epochChainUnverifiedReason) record.epoch_chain_unverified_reason = opts.epochChainUnverifiedReason;
3563
- }
3564
- return record;
3565
- }
3566
- _mergeGroupEntryFromLegacy(existing, incoming) {
3567
- const oldByEpoch = /* @__PURE__ */ new Map();
3568
- let current = existing && typeof existing.epoch === "number" ? deepClone(Object.fromEntries(
3569
- Object.entries(existing).filter(([key]) => key !== "old_epochs")
3570
- )) : null;
3571
- if (existing && Array.isArray(existing.old_epochs)) {
3572
- for (const rawOld of existing.old_epochs) {
3573
- if (typeof rawOld.epoch !== "number") continue;
3574
- oldByEpoch.set(rawOld.epoch, deepClone(rawOld));
3575
- }
3576
- }
3577
- if (typeof incoming.epoch === "number" && this._isGroupEpochRecoverable(incoming)) {
3578
- const incomingCurrent = deepClone(Object.fromEntries(
3579
- Object.entries(incoming).filter(([key]) => key !== "old_epochs")
3580
- ));
3581
- if (!current) {
3582
- current = incomingCurrent;
3583
- } else if (incomingCurrent.epoch > current.epoch) {
3584
- oldByEpoch.set(
3585
- current.epoch,
3586
- this._preferNewerGroupEpochRecord(oldByEpoch.get(current.epoch), current)
3587
- );
3588
- current = incomingCurrent;
3589
- } else if (incomingCurrent.epoch === current.epoch) {
3590
- current = this._preferNewerGroupEpochRecord(current, incomingCurrent);
3591
- } else {
3592
- oldByEpoch.set(
3593
- incomingCurrent.epoch,
3594
- this._preferNewerGroupEpochRecord(oldByEpoch.get(incomingCurrent.epoch), incomingCurrent)
3595
- );
3596
- }
3597
- }
3598
- const incomingOldEpochs = Array.isArray(incoming.old_epochs) ? incoming.old_epochs : [];
3599
- for (const rawOld of incomingOldEpochs) {
3600
- if (typeof rawOld.epoch !== "number" || !this._isGroupEpochRecoverable(rawOld)) continue;
3601
- oldByEpoch.set(rawOld.epoch, this._preferNewerGroupEpochRecord(oldByEpoch.get(rawOld.epoch), rawOld));
3602
- }
3603
- const merged = current ? deepClone(current) : {};
3604
- if (current && typeof current.epoch === "number") {
3605
- oldByEpoch.delete(current.epoch);
3606
- }
3607
- if (oldByEpoch.size > 0) {
3608
- merged.old_epochs = [...oldByEpoch.entries()].sort(([a], [b]) => a - b).map(([, value]) => deepClone(value));
3609
- }
3610
- return merged;
3611
- }
3612
- _preferNewerGroupEpochRecord(existing, incoming) {
3613
- if (!existing) return deepClone(incoming);
3614
- return Number(incoming.updated_at ?? 0) > Number(existing.updated_at ?? 0) ? deepClone(incoming) : deepClone(existing);
3615
- }
3616
- _isUnexpiredRecord(record, fallbackKey) {
3617
- const expiresAt = typeof record.expires_at === "number" ? record.expires_at : null;
3618
- if (expiresAt !== null) return expiresAt >= Date.now();
3619
- const marker = typeof record[fallbackKey] === "number" ? record[fallbackKey] : null;
3620
- return marker !== null && marker + STRUCTURED_RECOVERY_RETENTION_MS >= Date.now();
3621
- }
3622
- _isPrekeyRecoverable(record) {
3623
- return this._isUnexpiredRecord(record, "created_at");
3624
- }
3625
- _isGroupEpochRecoverable(record) {
3626
- const secret = record.secret;
3627
- if (!secret || typeof secret === "string" && secret.length === 0) return false;
3628
- return this._isUnexpiredRecord(record, "updated_at");
3629
- }
3630
- // ── Sessions 内部辅助 ─────────────────────────────────
3631
- async _loadSessionsUnlocked(aid) {
3632
- const items = await idbGetAllByPrefix(STORE_SESSIONS, sessionPrefix(aid));
3633
- const result = [];
3634
- for (const item of items) {
3635
- if (!isRecord(item.value)) continue;
3636
- result.push(deepClone(item.value));
3637
- }
3638
- return result;
3639
- }
3640
- async _migrateLegacySessionsUnlocked(aid) {
3641
- const metadataOnly = await this._loadMetadataOnlyUnlocked(aid) ?? {};
3642
- if (!Array.isArray(metadataOnly.e2ee_sessions) || metadataOnly.e2ee_sessions.length === 0) {
3643
- return;
3644
- }
3645
- for (const session of metadataOnly.e2ee_sessions) {
3646
- const sid = typeof session.session_id === "string" ? session.session_id : "";
3647
- if (!sid) continue;
3648
- const record = deepClone(session);
3649
- record.session_id = sid;
3650
- await idbPut(STORE_SESSIONS, sessionStoreKey(aid, sid), record);
3651
- }
3652
- const cleaned = deepClone(metadataOnly);
3653
- delete cleaned.e2ee_sessions;
3654
- await this._saveMetadataOnlyUnlocked(aid, cleaned);
3655
- }
3656
3086
  };
3657
3087
  __publicField(_IndexedDBTokenStore, "_aidTails", /* @__PURE__ */ new Map());
3658
3088
  // ── Trust Root Storage(信任根证书存储) ─────────────────
@@ -3925,7 +3355,7 @@ var AgentMdManager = class _AgentMdManager {
3925
3355
  }
3926
3356
  const etags = meta.agent_md_etags;
3927
3357
  if (!isJsonObject(etags)) return;
3928
- for (const key of ["requester", "peer", "receiver", "target", "to", "sender", "from"]) {
3358
+ for (const key of ["requester", "peer", "group", "receiver", "target", "to", "sender", "from"]) {
3929
3359
  const item = etags[key];
3930
3360
  if (!isJsonObject(item)) continue;
3931
3361
  await this.observeMeta(
@@ -3940,19 +3370,34 @@ var AgentMdManager = class _AgentMdManager {
3940
3370
  if (!isJsonObject(envelope)) return;
3941
3371
  if (!isJsonObject(envelope.agent_md)) return;
3942
3372
  const agentMd = envelope.agent_md;
3943
- if (!isJsonObject(agentMd.sender)) return;
3944
- const sender = agentMd.sender;
3945
- let senderAid = String(sender.aid ?? "").trim();
3946
- if (!senderAid) {
3947
- const aad = isJsonObject(envelope.aad) ? envelope.aad : {};
3948
- senderAid = String(aad.from ?? envelope.from ?? "").trim();
3949
- }
3950
- await this.observeMeta(
3951
- senderAid,
3952
- String(sender.etag ?? "").trim(),
3953
- String(sender.last_modified ?? sender.lastModified ?? "").trim(),
3954
- "envelope"
3955
- );
3373
+ if (isJsonObject(agentMd.sender)) {
3374
+ const sender = agentMd.sender;
3375
+ let senderAid = String(sender.aid ?? "").trim();
3376
+ if (!senderAid) {
3377
+ const aad = isJsonObject(envelope.aad) ? envelope.aad : {};
3378
+ senderAid = String(aad.from ?? envelope.from ?? "").trim();
3379
+ }
3380
+ await this.observeMeta(
3381
+ senderAid,
3382
+ String(sender.etag ?? "").trim(),
3383
+ String(sender.last_modified ?? sender.lastModified ?? "").trim(),
3384
+ "envelope.sender"
3385
+ );
3386
+ }
3387
+ if (isJsonObject(agentMd.group)) {
3388
+ const group = agentMd.group;
3389
+ let groupAid = String(group.aid ?? "").trim();
3390
+ if (!groupAid) {
3391
+ const aad = isJsonObject(envelope.aad) ? envelope.aad : {};
3392
+ groupAid = String(envelope.group_aid ?? envelope.group_id ?? aad.group_aid ?? aad.group_id ?? "").trim();
3393
+ }
3394
+ await this.observeMeta(
3395
+ groupAid,
3396
+ String(group.etag ?? "").trim(),
3397
+ String(group.last_modified ?? group.lastModified ?? "").trim(),
3398
+ "envelope.group"
3399
+ );
3400
+ }
3956
3401
  }
3957
3402
  eventSnapshot() {
3958
3403
  if (!this._localAgentMdEtag && !this._remoteAgentMdEtag) return null;
@@ -6877,11 +6322,35 @@ var RPCTransport = class {
6877
6322
  this._ws = ws;
6878
6323
  this._closed = false;
6879
6324
  let initialResolved = false;
6325
+ let connectTimeout = null;
6326
+ const clearConnectTimeout = () => {
6327
+ if (connectTimeout !== null) {
6328
+ clearTimeout(connectTimeout);
6329
+ connectTimeout = null;
6330
+ }
6331
+ };
6332
+ const cleanupHandshake = () => {
6333
+ ws.onopen = null;
6334
+ ws.onerror = null;
6335
+ ws.onmessage = null;
6336
+ ws.onclose = null;
6337
+ };
6338
+ const rollbackHandshake = () => {
6339
+ cleanupHandshake();
6340
+ this._ws = null;
6341
+ this._closed = true;
6342
+ try {
6343
+ ws.close();
6344
+ } catch {
6345
+ }
6346
+ };
6880
6347
  ws.onopen = () => {
6881
6348
  };
6882
6349
  ws.onerror = (event) => {
6883
6350
  if (!initialResolved) {
6884
6351
  initialResolved = true;
6352
+ clearConnectTimeout();
6353
+ rollbackHandshake();
6885
6354
  this._log.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=websocket_error`);
6886
6355
  reject(new ConnectionError(`websocket connection failed to ${url}`));
6887
6356
  }
@@ -6889,6 +6358,7 @@ var RPCTransport = class {
6889
6358
  ws.onmessage = (event) => {
6890
6359
  if (!initialResolved) {
6891
6360
  initialResolved = true;
6361
+ clearConnectTimeout();
6892
6362
  try {
6893
6363
  const message = this._decodeMessage(event.data);
6894
6364
  if (message.method === "challenge") {
@@ -6907,6 +6377,7 @@ var RPCTransport = class {
6907
6377
  resolve(null);
6908
6378
  }
6909
6379
  } catch (err) {
6380
+ rollbackHandshake();
6910
6381
  this._log.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
6911
6382
  reject(err instanceof Error ? err : new ConnectionError(String(err)));
6912
6383
  }
@@ -6918,10 +6389,22 @@ var RPCTransport = class {
6918
6389
  this._lastCloseReason = event.reason ?? "";
6919
6390
  if (!initialResolved) {
6920
6391
  initialResolved = true;
6392
+ clearConnectTimeout();
6393
+ cleanupHandshake();
6394
+ this._ws = null;
6395
+ this._closed = true;
6921
6396
  this._log.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=closed_before_initial code=${event.code}`);
6922
6397
  reject(new ConnectionError(`websocket closed before initial message: code=${event.code}`));
6923
6398
  }
6924
6399
  };
6400
+ connectTimeout = globalThis.setTimeout(() => {
6401
+ if (!initialResolved) {
6402
+ initialResolved = true;
6403
+ rollbackHandshake();
6404
+ this._log.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=connect_timeout`);
6405
+ reject(new ConnectionError("websocket connect timeout"));
6406
+ }
6407
+ }, this._timeout * 1e3);
6925
6408
  });
6926
6409
  }
6927
6410
  /** 关闭连接 */
@@ -7103,18 +6586,64 @@ var RPCTransport = class {
7103
6586
  return run;
7104
6587
  }
7105
6588
  _sendText(payload, context, beforeSend) {
7106
- return this._enqueueSend(() => {
6589
+ return this._enqueueSend(async () => {
7107
6590
  if (beforeSend && !beforeSend()) {
7108
6591
  throw new ConnectionError(`send cancelled before write: ${context}`);
7109
6592
  }
7110
6593
  if (this._closed || !this._ws) {
7111
6594
  throw this._notConnectedError();
7112
6595
  }
6596
+ const ws = this._ws;
7113
6597
  try {
7114
- this._ws.send(payload);
6598
+ ws.send(payload);
7115
6599
  } catch (err) {
7116
- throw new ConnectionError(`failed to send ${context}: ${err instanceof Error ? err.message : String(err)}`);
6600
+ throw await this._sendFailureError(context, err, ws);
6601
+ }
6602
+ });
6603
+ }
6604
+ async _sendFailureError(context, err, ws) {
6605
+ const original = err instanceof Error ? err.message : String(err);
6606
+ const closeCode = await this._waitForCloseCodeIfClosing(ws);
6607
+ if (closeCode !== null) {
6608
+ const reason = this._lastCloseReason ? ` reason=${this._lastCloseReason}` : "";
6609
+ return new ConnectionError(`failed to send ${context}: websocket closed: code=${closeCode}${reason}; original=${original}`, {
6610
+ code: closeCode
6611
+ });
6612
+ }
6613
+ return new ConnectionError(`failed to send ${context}: ${original}`);
6614
+ }
6615
+ async _waitForCloseCodeIfClosing(ws) {
6616
+ if (this._lastCloseCode !== null) return this._lastCloseCode;
6617
+ if (!ws) return null;
6618
+ const readyState = Number(ws.readyState);
6619
+ const closingState = typeof WebSocket !== "undefined" ? WebSocket.CLOSING : 2;
6620
+ const closedState = typeof WebSocket !== "undefined" ? WebSocket.CLOSED : 3;
6621
+ if (readyState !== closingState && readyState !== closedState) return null;
6622
+ return await new Promise((resolve) => {
6623
+ let settled = false;
6624
+ let timer = null;
6625
+ const finish = (code) => {
6626
+ if (settled) return;
6627
+ settled = true;
6628
+ if (timer !== null) clearTimeout(timer);
6629
+ try {
6630
+ ws.removeEventListener("close", onClose);
6631
+ } catch {
6632
+ }
6633
+ if (Number.isFinite(code)) {
6634
+ resolve(Number(code));
6635
+ return;
6636
+ }
6637
+ resolve(this._lastCloseCode);
6638
+ };
6639
+ const onClose = (event) => finish(event.code);
6640
+ try {
6641
+ ws.addEventListener("close", onClose);
6642
+ } catch {
6643
+ resolve(this._lastCloseCode);
6644
+ return;
7117
6645
  }
6646
+ timer = setTimeout(() => finish(), 100);
7118
6647
  });
7119
6648
  }
7120
6649
  /** 从 pending / queue 中移除指定 RPC */
@@ -7849,6 +7378,10 @@ var ClientRuntime = class {
7849
7378
  var PUSHED_SEQS_LIMIT = 5e4;
7850
7379
  var PENDING_ORDERED_LIMIT = 5e4;
7851
7380
  var GROUP_RECALL_SEEN_LIMIT = 1e4;
7381
+ var SEQ_TRACKER_PERSIST_FLUSH_DELAY_MS = 200;
7382
+ var P2P_GAP_FILL_RETRY_LIMIT = 12;
7383
+ var P2P_GAP_FILL_RETRY_BASE_MS = 1e3;
7384
+ var P2P_GAP_FILL_RETRY_MAX_MS = 5e3;
7852
7385
  var APP_MESSAGE_ENVELOPE_KEYS = [
7853
7386
  "module_id",
7854
7387
  "message_type",
@@ -7898,6 +7431,24 @@ var APP_GROUP_EVENT_ENVELOPE_KEYS = [
7898
7431
  function formatDeliveryError(error) {
7899
7432
  return error instanceof Error ? error : String(error);
7900
7433
  }
7434
+ function isTransientGapFillError(error) {
7435
+ const text3 = (error instanceof Error ? error.message : String(error)).toLowerCase();
7436
+ return text3.includes("no_service") || text3.includes("service plane unavailable") || text3.includes("transport not connected") || text3.includes("transport closed") || text3.includes("websocket closed") || text3.includes("websocket is not open") || text3.includes("connection closed") || text3.includes("socket closed");
7437
+ }
7438
+ function p2pAppEventFromPlainPullMessage(message) {
7439
+ const payload = message.payload;
7440
+ const payloadType = isJsonObject(payload) ? String(payload.type ?? "").trim() : "";
7441
+ if (payloadType === "message.recalled" || payloadType === "message.recall" || message.type === "message.recalled") {
7442
+ return {
7443
+ event: "message.recalled",
7444
+ payload: {
7445
+ ...message,
7446
+ ...isJsonObject(payload) ? payload : {}
7447
+ }
7448
+ };
7449
+ }
7450
+ return { event: "message.received", payload: message };
7451
+ }
7901
7452
  var MessageDeliveryEngine = class {
7902
7453
  constructor(runtime) {
7903
7454
  __publicField(this, "runtime");
@@ -7981,7 +7532,8 @@ var MessageDeliveryEngine = class {
7981
7532
  const eventPayload = payload;
7982
7533
  client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
7983
7534
  if (eventPayload.action === "dissolved") {
7984
- const groupId = normalizeGroupId(String(eventPayload.group_id ?? "")) || String(eventPayload.group_id ?? "").trim();
7535
+ const rawGroupId = eventPayload.group_aid ?? eventPayload.group_id ?? "";
7536
+ const groupId = normalizeGroupId(String(rawGroupId)) || String(rawGroupId).trim();
7985
7537
  if (groupId) client._cleanupDissolvedGroup?.(groupId);
7986
7538
  }
7987
7539
  }
@@ -8305,7 +7857,7 @@ var MessageDeliveryEngine = class {
8305
7857
  client._clientLog.warn("group recall auto-ack failed: group=" + groupId, e);
8306
7858
  });
8307
7859
  }
8308
- if (contig !== contigBefore) this.saveSeqTrackerState();
7860
+ if (contig !== contigBefore) this.persistSeq(ns);
8309
7861
  }
8310
7862
  async publishAppEvent(event, payload) {
8311
7863
  const client = this.runtime.client;
@@ -8388,7 +7940,7 @@ var MessageDeliveryEngine = class {
8388
7940
  client._clientLog.warn(`P2P auto-ack failed:${String(e)}`);
8389
7941
  });
8390
7942
  }
8391
- if (contigAfter !== contigBefore) this.saveSeqTrackerState();
7943
+ if (contigAfter !== contigBefore) this.persistSeq(ns);
8392
7944
  if (encryptedPush) return;
8393
7945
  } else {
8394
7946
  if (encryptedPush) {
@@ -8461,7 +8013,7 @@ var MessageDeliveryEngine = class {
8461
8013
  client._clientLog.warn("group recall auto-ack failed: group=" + groupId, e);
8462
8014
  });
8463
8015
  }
8464
- if (contigAfter2 !== contigBefore) this.saveSeqTrackerState();
8016
+ if (contigAfter2 !== contigBefore) this.persistSeq(ns);
8465
8017
  return;
8466
8018
  }
8467
8019
  const published = encryptedPush ? await client._publishEncryptedPushMessage("group.message_created", "group.message_undecryptable", ns, seq, msg, true) : await this.publishOrderedMessage("group.message_created", ns, seq, msg);
@@ -8483,7 +8035,7 @@ var MessageDeliveryEngine = class {
8483
8035
  client._clientLog.warn("group message auto-ack failed: group=" + groupId, e);
8484
8036
  });
8485
8037
  }
8486
- if (contigAfter !== contigBefore) this.saveSeqTrackerState();
8038
+ if (contigAfter !== contigBefore) this.persistSeq(ns);
8487
8039
  if (encryptedPush) return;
8488
8040
  } else {
8489
8041
  if (encryptedPush) {
@@ -8561,9 +8113,26 @@ var MessageDeliveryEngine = class {
8561
8113
  }
8562
8114
  await client._publishAppEvent("group.message_created", notification);
8563
8115
  }
8564
- async fillP2pGap() {
8116
+ scheduleP2pGapRetry(ns, retryAttempt) {
8117
+ const client = this.runtime.client;
8118
+ if (retryAttempt > P2P_GAP_FILL_RETRY_LIMIT) return;
8119
+ const retryKey = `p2p_pull_retry:${ns}`;
8120
+ if (client._gapFillDone.has(retryKey)) return;
8121
+ const delayMs = Math.min(
8122
+ P2P_GAP_FILL_RETRY_BASE_MS * retryAttempt,
8123
+ P2P_GAP_FILL_RETRY_MAX_MS
8124
+ );
8125
+ client._gapFillDone.add(retryKey);
8126
+ client._clientLog.debug(`P2P message gap-fill retry scheduled: ns=${ns} attempt=${retryAttempt} delay_ms=${delayMs}`);
8127
+ globalThis.setTimeout(() => {
8128
+ client._gapFillDone.delete(retryKey);
8129
+ if (client.state !== "ready" /* READY */ || client._closing) return;
8130
+ client._safeAsync(this.fillP2pGap(retryAttempt));
8131
+ }, delayMs);
8132
+ }
8133
+ async fillP2pGap(retryAttempt = 0) {
8565
8134
  const client = this.runtime.client;
8566
- if (client._state !== "connected" || client._closing) return;
8135
+ if (client.state !== "ready" /* READY */ || client._closing) return;
8567
8136
  if (!client._aid) return;
8568
8137
  const ns = `p2p:${client._aid}`;
8569
8138
  const afterSeq = client._seqTracker.getContiguousSeq(ns);
@@ -8577,18 +8146,51 @@ var MessageDeliveryEngine = class {
8577
8146
  filled = messages.length;
8578
8147
  this.prunePushedSeqs(ns);
8579
8148
  } catch (exc) {
8580
- client._clientLog.warn(`P2P message gap-fill failed:${String(formatDeliveryError(exc))}`);
8149
+ const transient = isTransientGapFillError(exc);
8150
+ if (transient) {
8151
+ try {
8152
+ filled = await this.fillP2pGapWithPlainPull(ns, afterSeq);
8153
+ this.prunePushedSeqs(ns);
8154
+ client._clientLog.warn(`P2P message gap-fill V2 unavailable, used plain pull fallback:${String(formatDeliveryError(exc))}`);
8155
+ } catch (fallbackExc) {
8156
+ client._clientLog.warn(`P2P message gap-fill plain fallback failed:${String(formatDeliveryError(fallbackExc))}`);
8157
+ this.scheduleP2pGapRetry(ns, retryAttempt + 1);
8158
+ }
8159
+ } else {
8160
+ client._clientLog.warn(`P2P message gap-fill failed:${String(formatDeliveryError(exc))}`);
8161
+ }
8581
8162
  } finally {
8582
8163
  client._gapFillDone.delete(dedupKey);
8583
8164
  this.runtime.delivery.setGapFillActive(false);
8584
- if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
8585
- client._safeAsync(this.fillP2pGap());
8165
+ }
8166
+ }
8167
+ async fillP2pGapWithPlainPull(ns, afterSeq) {
8168
+ const client = this.runtime.client;
8169
+ const params2 = {
8170
+ after_seq: afterSeq,
8171
+ limit: 50,
8172
+ device_id: client._deviceId,
8173
+ slot_id: client._slotId,
8174
+ _rpc_background: true
8175
+ };
8176
+ const raw = await client._transport.call("message.pull", params2, void 0, void 0, true);
8177
+ const result = typeof client._rpcPipeline?.postprocessResult === "function" ? await client._rpcPipeline.postprocessResult("message.pull", params2, raw) : raw;
8178
+ const messages = isJsonObject(result) && Array.isArray(result.messages) ? result.messages : [];
8179
+ let published = 0;
8180
+ for (const item of messages) {
8181
+ if (!isJsonObject(item)) continue;
8182
+ const msg = item;
8183
+ const seq = Number(msg.seq ?? 0);
8184
+ const appEvent = p2pAppEventFromPlainPullMessage(msg);
8185
+ if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload)) {
8186
+ published += 1;
8586
8187
  }
8587
8188
  }
8189
+ return published;
8588
8190
  }
8589
8191
  async fillGroupGap(groupId) {
8590
8192
  const client = this.runtime.client;
8591
- if (client._state !== "connected" || client._closing) return;
8193
+ if (client.state !== "ready" /* READY */ || client._closing) return;
8592
8194
  groupId = normalizeGroupId(groupId) || String(groupId ?? "").trim();
8593
8195
  if (!groupId) return;
8594
8196
  const ns = `group:${groupId}`;
@@ -8614,7 +8216,9 @@ var MessageDeliveryEngine = class {
8614
8216
  }
8615
8217
  async fillGroupEventGap(groupId) {
8616
8218
  const client = this.runtime.client;
8617
- if (client._state !== "connected" || client._closing) return;
8219
+ if (client.state !== "ready" /* READY */ || client._closing) return;
8220
+ groupId = normalizeGroupId(groupId) || String(groupId ?? "").trim();
8221
+ if (!groupId) return;
8618
8222
  const ns = `group_event:${groupId}`;
8619
8223
  const afterSeq = client._seqTracker.getContiguousSeq(ns);
8620
8224
  const dedupKey = `group_event_pull:${ns}`;
@@ -8676,7 +8280,7 @@ var MessageDeliveryEngine = class {
8676
8280
  const ackContig = client._seqTracker.getContiguousSeq(ns);
8677
8281
  await this.drainOrderedMessages(ns);
8678
8282
  if (ackContig !== pageContigBefore && !hasDissolvedEvent) {
8679
- this.saveSeqTrackerState();
8283
+ this.persistSeq(ns);
8680
8284
  }
8681
8285
  if (eventObjects.length > 0 && ackContig > 0 && ackContig !== pageContigBefore) {
8682
8286
  const ackSeq = this.clampAckSeq("group.ack_events", "event_seq", ns, ackContig);
@@ -8706,6 +8310,7 @@ var MessageDeliveryEngine = class {
8706
8310
  }
8707
8311
  async handleGroupChangedEventSeq(data, groupId) {
8708
8312
  const client = this.runtime.client;
8313
+ groupId = normalizeGroupId(groupId) || String(groupId ?? "").trim();
8709
8314
  let needPull = false;
8710
8315
  const rawEventSeq = data.event_seq;
8711
8316
  const eventSeq = Number(rawEventSeq);
@@ -8738,7 +8343,7 @@ var MessageDeliveryEngine = class {
8738
8343
  const ackContig = client._seqTracker.getContiguousSeq(ns);
8739
8344
  await this.drainOrderedMessages(ns);
8740
8345
  if (ackContig > 0 && ackContig !== contigBefore) {
8741
- if (data.action !== "dissolved") this.saveSeqTrackerState();
8346
+ if (data.action !== "dissolved") this.persistSeq(ns);
8742
8347
  const ackSeq = this.clampAckSeq("group.ack_events", "event_seq", ns, ackContig);
8743
8348
  client._transport.call("group.ack_events", {
8744
8349
  group_id: groupId,
@@ -8782,7 +8387,7 @@ var MessageDeliveryEngine = class {
8782
8387
  this.runtime.delivery.setOnlineUnreadHintDrainActive(true);
8783
8388
  try {
8784
8389
  while (client._onlineUnreadHintQueue.size > 0) {
8785
- if (client.state !== "ready") return;
8390
+ if (client.state !== "ready" /* READY */) return;
8786
8391
  if (client._sessionOptions?.background_sync === false) return;
8787
8392
  const groupId = client._onlineUnreadHintQueue.keys().next().value;
8788
8393
  if (!groupId) return;
@@ -8900,7 +8505,7 @@ var MessageDeliveryEngine = class {
8900
8505
  const published = await client._publishOrderedMessage("message.received", ns, pushSeq, decrypted);
8901
8506
  const newContig = client._seqTracker.getContiguousSeq(ns);
8902
8507
  if (newContig !== contigBefore) {
8903
- client._saveSeqTrackerState();
8508
+ this.persistSeq(ns);
8904
8509
  }
8905
8510
  if (newContig > 0 && newContig !== contigBefore) {
8906
8511
  const ackSeq = this.clampAckSeq("message.v2.ack", "up_to_seq", ns, newContig);
@@ -9060,49 +8665,130 @@ var MessageDeliveryEngine = class {
9060
8665
  }
9061
8666
  return newState;
9062
8667
  }
9063
- saveSeqTrackerState() {
8668
+ async saveSeqTrackerState() {
9064
8669
  const client = this.runtime.client;
9065
- if (!client._aid) return;
8670
+ const context = this.currentSeqTrackerContext();
8671
+ if (!context) {
8672
+ await this.flushSeqTrackerPending();
8673
+ return;
8674
+ }
9066
8675
  const state = client._seqTracker.exportState();
9067
- if (Object.keys(state).length === 0) return;
8676
+ const pending = this.seqTrackerPending();
8677
+ if (pending.size > 0 && client._seqTrackerPendingPersistContext === context) {
8678
+ const liveNamespaces = new Set(Object.keys(state));
8679
+ for (const ns of [...pending.keys()]) {
8680
+ if (!liveNamespaces.has(ns)) pending.delete(ns);
8681
+ }
8682
+ if (pending.size === 0) client._seqTrackerPendingPersistContext = null;
8683
+ }
8684
+ if (Object.keys(state).length > 0) this.mergeSeqTrackerPending(context, state);
8685
+ await this.flushSeqTrackerPending();
8686
+ }
8687
+ persistSeq(ns, forceSeq) {
8688
+ const client = this.runtime.client;
8689
+ const context = this.currentSeqTrackerContext();
8690
+ if (!context) {
8691
+ void this.flushSeqTrackerPending();
8692
+ return;
8693
+ }
8694
+ const seq = forceSeq ?? client._seqTracker.getContiguousSeq(ns);
8695
+ if (!ns || !Number.isFinite(seq) || seq <= 0) return;
8696
+ if (forceSeq !== void 0) {
8697
+ client._seqTracker.forceContiguousSeq(ns, seq);
8698
+ }
8699
+ this.mergeSeqTrackerPending(context, { [ns]: seq });
8700
+ this.scheduleSeqTrackerFlush();
8701
+ }
8702
+ async flushSeqTrackerPending() {
8703
+ const client = this.runtime.client;
8704
+ this.cancelSeqTrackerFlushTimer();
8705
+ const context = client._seqTrackerPendingPersistContext;
8706
+ const pending = this.seqTrackerPending();
8707
+ if (!context || pending.size === 0) {
8708
+ client._seqTrackerPendingPersistContext = null;
8709
+ return;
8710
+ }
8711
+ const [aid, deviceId, slotId] = JSON.parse(context);
8712
+ const state = Object.fromEntries(pending.entries());
8713
+ pending.clear();
8714
+ client._seqTrackerPendingPersistContext = null;
8715
+ await this.writeSeqTrackerState(aid, deviceId, slotId, state);
8716
+ }
8717
+ dropSeqTrackerPending(ns) {
8718
+ const client = this.runtime.client;
8719
+ const pending = this.seqTrackerPending();
8720
+ pending.delete(ns);
8721
+ if (pending.size === 0) {
8722
+ client._seqTrackerPendingPersistContext = null;
8723
+ this.cancelSeqTrackerFlushTimer();
8724
+ }
8725
+ }
8726
+ currentSeqTrackerContext() {
8727
+ const client = this.runtime.client;
8728
+ if (!client._aid) return null;
8729
+ return JSON.stringify([client._aid, client._deviceId, client._slotId]);
8730
+ }
8731
+ seqTrackerPending() {
8732
+ const client = this.runtime.client;
8733
+ if (!(client._seqTrackerPendingPersist instanceof Map)) {
8734
+ client._seqTrackerPendingPersist = /* @__PURE__ */ new Map();
8735
+ }
8736
+ return client._seqTrackerPendingPersist;
8737
+ }
8738
+ mergeSeqTrackerPending(context, state) {
8739
+ const client = this.runtime.client;
8740
+ const pending = this.seqTrackerPending();
8741
+ const pendingContext = client._seqTrackerPendingPersistContext;
8742
+ if (pendingContext && pendingContext !== context && pending.size > 0) {
8743
+ void this.flushSeqTrackerPending();
8744
+ }
8745
+ client._seqTrackerPendingPersistContext = context;
8746
+ for (const [ns, seq] of Object.entries(state)) {
8747
+ const seqNum = Number(seq);
8748
+ if (ns && Number.isFinite(seqNum) && seqNum > 0) {
8749
+ pending.set(ns, seqNum);
8750
+ }
8751
+ }
8752
+ }
8753
+ scheduleSeqTrackerFlush() {
8754
+ const client = this.runtime.client;
8755
+ const pending = this.seqTrackerPending();
8756
+ if (pending.size === 0) return;
8757
+ if (client._seqTrackerFlushTimer) return;
8758
+ client._seqTrackerFlushTimer = setTimeout(() => {
8759
+ client._seqTrackerFlushTimer = null;
8760
+ void this.flushSeqTrackerPending();
8761
+ }, SEQ_TRACKER_PERSIST_FLUSH_DELAY_MS);
8762
+ }
8763
+ cancelSeqTrackerFlushTimer() {
8764
+ const client = this.runtime.client;
8765
+ if (client._seqTrackerFlushTimer) {
8766
+ clearTimeout(client._seqTrackerFlushTimer);
8767
+ client._seqTrackerFlushTimer = null;
8768
+ }
8769
+ }
8770
+ async writeSeqTrackerState(aid, deviceId, slotId, state) {
8771
+ const client = this.runtime.client;
8772
+ if (!aid || Object.keys(state).length === 0) return;
9068
8773
  try {
9069
8774
  const saveFn = client._tokenStore.saveSeq?.bind(client._tokenStore);
9070
8775
  if (typeof saveFn === "function") {
9071
- for (const [ns, seq] of Object.entries(state)) {
9072
- saveFn(client._aid, client._deviceId, client._slotId, ns, seq).catch((exc) => {
9073
- client._dispatcher.publish("seq_tracker.persist_error", {
9074
- phase: "save",
9075
- aid: client._aid,
9076
- device_id: client._deviceId,
9077
- slot_id: client._slotId,
9078
- error: String(exc)
9079
- }).catch(() => {
9080
- });
9081
- });
9082
- }
8776
+ await Promise.all(Object.entries(state).map(([ns, seq]) => saveFn(aid, deviceId, slotId, ns, seq)));
9083
8777
  return;
9084
8778
  }
9085
8779
  if (typeof client._tokenStore.updateInstanceState === "function") {
9086
- client._tokenStore.updateInstanceState(client._aid, client._deviceId, client._slotId, (current) => {
9087
- current.seq_tracker_state = state;
8780
+ await client._tokenStore.updateInstanceState(aid, deviceId, slotId, (current) => {
8781
+ const existing = isJsonObject(current.seq_tracker_state) ? { ...current.seq_tracker_state } : {};
8782
+ current.seq_tracker_state = { ...existing, ...state };
9088
8783
  return current;
9089
- }).catch((exc) => {
9090
- client._dispatcher.publish("seq_tracker.persist_error", {
9091
- phase: "save",
9092
- aid: client._aid,
9093
- device_id: client._deviceId,
9094
- slot_id: client._slotId,
9095
- error: String(exc)
9096
- }).catch(() => {
9097
- });
9098
8784
  });
9099
8785
  }
9100
8786
  } catch (exc) {
9101
8787
  client._dispatcher.publish("seq_tracker.persist_error", {
9102
8788
  phase: "save",
9103
- aid: client._aid,
9104
- device_id: client._deviceId,
9105
- slot_id: client._slotId,
8789
+ aid,
8790
+ device_id: deviceId,
8791
+ slot_id: slotId,
9106
8792
  error: String(exc)
9107
8793
  }).catch(() => {
9108
8794
  });
@@ -9112,6 +8798,7 @@ var MessageDeliveryEngine = class {
9112
8798
  const client = this.runtime.client;
9113
8799
  if (!client._aid || !ns) return;
9114
8800
  const seq = client._seqTracker.getContiguousSeq(ns);
8801
+ this.dropSeqTrackerPending(ns);
9115
8802
  try {
9116
8803
  if (seq > 0 && typeof client._tokenStore.saveSeq === "function") {
9117
8804
  client._tokenStore.saveSeq(client._aid, client._deviceId, client._slotId, ns, seq).catch((exc) => {
@@ -9126,8 +8813,21 @@ var MessageDeliveryEngine = class {
9126
8813
  });
9127
8814
  return;
9128
8815
  }
8816
+ if (seq <= 0 && typeof client._tokenStore.updateInstanceState === "function") {
8817
+ Promise.resolve(client._tokenStore.updateInstanceState(client._aid, client._deviceId, client._slotId, (current) => {
8818
+ if (isJsonObject(current.seq_tracker_state)) {
8819
+ const next = { ...current.seq_tracker_state };
8820
+ delete next[ns];
8821
+ current.seq_tracker_state = next;
8822
+ }
8823
+ return current;
8824
+ })).catch((exc) => {
8825
+ client._clientLog.debug(`delete repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
8826
+ });
8827
+ return;
8828
+ }
9129
8829
  if (seq > 0) {
9130
- this.saveSeqTrackerState();
8830
+ void this.saveSeqTrackerState();
9131
8831
  }
9132
8832
  } catch (exc) {
9133
8833
  client._clientLog.debug(`persist repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
@@ -9179,14 +8879,19 @@ var MessageDeliveryEngine = class {
9179
8879
  if (!queue || queue.size === 0) return;
9180
8880
  const contig = client._seqTracker.getContiguousSeq(ns);
9181
8881
  const ready = [...queue.keys()].filter((seq) => seq <= contig && (beforeSeq === void 0 || seq < beforeSeq)).sort((a, b) => a - b);
8882
+ let delivered = false;
9182
8883
  for (const seq of ready) {
9183
8884
  const item = queue.get(seq);
9184
8885
  queue.delete(seq);
9185
8886
  if (!item || client._pushedSeqs.get(ns)?.has(seq)) continue;
9186
8887
  await this.publishOrderedQueueItem(ns, item.event, seq, item.payload);
9187
8888
  this.markPublishedSeq(ns, seq);
8889
+ delivered = true;
8890
+ }
8891
+ if (queue.size === 0) {
8892
+ client._pendingOrderedMsgs.delete(ns);
8893
+ if (delivered) await this.saveSeqTrackerState();
9188
8894
  }
9189
- if (queue.size === 0) client._pendingOrderedMsgs.delete(ns);
9190
8895
  }
9191
8896
  async publishOrderedMessage(event, ns, seq, payload) {
9192
8897
  const client = this.runtime.client;
@@ -9214,6 +8919,7 @@ var MessageDeliveryEngine = class {
9214
8919
  await this.publishOrderedQueueItem(ns, event, seqNum, payload);
9215
8920
  this.markPublishedSeq(ns, seqNum);
9216
8921
  await this.drainOrderedMessages(ns);
8922
+ if (!client._pendingOrderedMsgs.get(ns)) await this.saveSeqTrackerState();
9217
8923
  return true;
9218
8924
  }
9219
8925
  async publishPulledMessage(event, ns, seq, payload) {
@@ -9478,7 +9184,7 @@ var LifecycleController = class {
9478
9184
  client._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms reason=not_connected`);
9479
9185
  return;
9480
9186
  }
9481
- client._saveSeqTrackerState();
9187
+ await client._saveSeqTrackerState();
9482
9188
  client._stopBackgroundTasks();
9483
9189
  if (client._reconnectAbort) {
9484
9190
  client._reconnectAbort.abort();
@@ -9494,7 +9200,7 @@ var LifecycleController = class {
9494
9200
  const tStart = Date.now();
9495
9201
  client._clientLog.debug(`close enter: state=${client._state}`);
9496
9202
  this.runtime.lifecycle.setClosing(true);
9497
- client._saveSeqTrackerState();
9203
+ await client._saveSeqTrackerState();
9498
9204
  client._stopBackgroundTasks();
9499
9205
  if (client._reconnectAbort) {
9500
9206
  client._reconnectAbort.abort();
@@ -9563,9 +9269,6 @@ var INTERNAL_ONLY_METHODS = /* @__PURE__ */ new Set([
9563
9269
  "auth.refresh_token",
9564
9270
  "initialize"
9565
9271
  ]);
9566
- var REMOVED_E2EE_METHODS = /* @__PURE__ */ new Set([
9567
- "group.rotate_epoch"
9568
- ]);
9569
9272
  var PROTECTED_HEADERS_METHODS = /* @__PURE__ */ new Set([
9570
9273
  "message.send",
9571
9274
  "group.send",
@@ -9592,13 +9295,11 @@ var SIGNED_METHODS = /* @__PURE__ */ new Set([
9592
9295
  "group.add_member",
9593
9296
  "group.leave",
9594
9297
  "group.remove_member",
9595
- "group.update_rules",
9596
9298
  "group.update",
9597
- "group.update_announcement",
9598
- "group.update_join_requirements",
9599
9299
  "group.set_role",
9600
9300
  "group.transfer_owner",
9601
9301
  "group.bind_group_aid",
9302
+ "group.renew_group_aid",
9602
9303
  "group.complete_transfer",
9603
9304
  "group.review_join_request",
9604
9305
  "group.batch_review_join_request",
@@ -9607,10 +9308,14 @@ var SIGNED_METHODS = /* @__PURE__ */ new Set([
9607
9308
  "group.thought.put",
9608
9309
  "message.thought.put",
9609
9310
  "group.set_settings",
9311
+ "group.update_announcement",
9312
+ "group.update_rules",
9610
9313
  "group.fs.mkdir",
9611
9314
  "group.fs.rm",
9612
9315
  "group.fs.cp",
9613
9316
  "group.fs.mv",
9317
+ "group.fs.set_acl",
9318
+ "group.fs.remove_acl",
9614
9319
  "group.fs.mount",
9615
9320
  "group.fs.umount",
9616
9321
  "group.fs.check_upload",
@@ -9671,6 +9376,59 @@ var SIGNED_METHODS = /* @__PURE__ */ new Set([
9671
9376
  "group.suspend",
9672
9377
  "group.resume"
9673
9378
  ]);
9379
+ var SIGNING_KEY_CACHE_MAX = 32;
9380
+ var signingKeyCache = /* @__PURE__ */ new Map();
9381
+ var certFingerprintCache = /* @__PURE__ */ new Map();
9382
+ function cacheGet(cache, key) {
9383
+ const value = cache.get(key);
9384
+ if (value !== void 0) {
9385
+ cache.delete(key);
9386
+ cache.set(key, value);
9387
+ }
9388
+ return value;
9389
+ }
9390
+ function cachePut(cache, key, value) {
9391
+ cache.set(key, value);
9392
+ while (cache.size > SIGNING_KEY_CACHE_MAX) {
9393
+ const first = cache.keys().next().value;
9394
+ if (first === void 0) break;
9395
+ cache.delete(first);
9396
+ }
9397
+ return value;
9398
+ }
9399
+ async function sha256HexText(text3) {
9400
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text3));
9401
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
9402
+ }
9403
+ async function signingPrivateKey(privateKeyPem) {
9404
+ const key = await sha256HexText(privateKeyPem);
9405
+ let cached = cacheGet(signingKeyCache, key);
9406
+ if (!cached) {
9407
+ const pkcs8 = pemToArrayBuffer(privateKeyPem);
9408
+ cached = crypto.subtle.importKey(
9409
+ "pkcs8",
9410
+ pkcs8,
9411
+ { name: "ECDSA", namedCurve: "P-256" },
9412
+ false,
9413
+ ["sign"]
9414
+ );
9415
+ cachePut(signingKeyCache, key, cached);
9416
+ }
9417
+ return await cached;
9418
+ }
9419
+ async function signingCertFingerprint(certPem) {
9420
+ const key = await sha256HexText(certPem);
9421
+ let cached = cacheGet(certFingerprintCache, key);
9422
+ if (!cached) {
9423
+ cached = (async () => {
9424
+ const certDer = pemToArrayBuffer(certPem);
9425
+ const fpBuf = await crypto.subtle.digest("SHA-256", certDer);
9426
+ return "sha256:" + Array.from(new Uint8Array(fpBuf)).map((b) => b.toString(16).padStart(2, "0")).join("");
9427
+ })();
9428
+ cachePut(certFingerprintCache, key, cached);
9429
+ }
9430
+ return await cached;
9431
+ }
9674
9432
  var PULL_GATE_STALE_MS = 3e4;
9675
9433
  var NON_IDEMPOTENT_TIMEOUT = 35;
9676
9434
  var NON_IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
@@ -9682,10 +9440,9 @@ var NON_IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
9682
9440
  "group.remove_member",
9683
9441
  "group.leave",
9684
9442
  "group.dissolve",
9685
- "group.update_name",
9686
- "group.update_avatar",
9443
+ "group.set_settings",
9687
9444
  "group.update_announcement",
9688
- "group.update_settings",
9445
+ "group.update_rules",
9689
9446
  "storage.put_object",
9690
9447
  "storage.delete_object",
9691
9448
  "storage.create_share_link",
@@ -9735,6 +9492,8 @@ var NON_IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
9735
9492
  "group.fs.rm",
9736
9493
  "group.fs.cp",
9737
9494
  "group.fs.mv",
9495
+ "group.fs.set_acl",
9496
+ "group.fs.remove_acl",
9738
9497
  "group.fs.mount",
9739
9498
  "group.fs.umount",
9740
9499
  "group.fs.check_upload",
@@ -9751,6 +9510,16 @@ var NON_IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
9751
9510
  "collab.tag.rm",
9752
9511
  "collab.tag.prune"
9753
9512
  ]);
9513
+ function hasMatchingV2Session(client) {
9514
+ const session = client._v2Session;
9515
+ if (!session) return false;
9516
+ if (session.currentIkPubDer === void 0) return true;
9517
+ return session.aid === client._aid && session.deviceId === client._deviceId;
9518
+ }
9519
+ function isTransientV2PullFallbackError(error) {
9520
+ const text3 = (error instanceof Error ? error.message : String(error)).toLowerCase();
9521
+ return text3.includes("no_service") || text3.includes("service plane unavailable");
9522
+ }
9754
9523
  var RpcPipeline = class {
9755
9524
  constructor(runtime) {
9756
9525
  __publicField(this, "runtime");
@@ -9807,7 +9576,7 @@ var RpcPipeline = class {
9807
9576
  "V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)"
9808
9577
  );
9809
9578
  client._clientLog.debug("call route: group.send -> V2 encrypted send");
9810
- return await client._sendGroupV2(String(p.group_id ?? ""), p.payload ?? {}, {
9579
+ return await client._sendGroupV2(String(p.group_aid ?? p.group_id ?? ""), p.payload ?? {}, {
9811
9580
  messageId: String(p.message_id ?? "") || void 0,
9812
9581
  timestamp: p.timestamp,
9813
9582
  protectedHeaders: client._protectedHeadersFromParams(p),
@@ -9850,20 +9619,27 @@ var RpcPipeline = class {
9850
9619
  }
9851
9620
  async callImplInner(method, p, skipSendResultEnvelope = false) {
9852
9621
  const client = this.runtime.client;
9853
- if (method === "message.pull") {
9854
- await client._ensureV2SessionReady("message.pull");
9855
- client._clientLog.debug("call route: message.pull -> V2 pull");
9856
- const messages = await client._pullV2(Number(p.after_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, { force: p.force === true });
9857
- return { messages };
9622
+ if (method === "message.v2.pull" || method === "message.pull" && hasMatchingV2Session(client)) {
9623
+ try {
9624
+ await client._ensureV2SessionReady("message.pull");
9625
+ client._clientLog.debug(`call route: ${method} -> V2 pull`);
9626
+ const messages = await client._pullV2(Number(p.after_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, { force: p.force === true });
9627
+ return { messages };
9628
+ } catch (exc) {
9629
+ if (method !== "message.pull" || !isTransientV2PullFallbackError(exc)) {
9630
+ throw exc;
9631
+ }
9632
+ client._clientLog.warn(`message.pull V2 route unavailable, fallback to plain pull: ${exc instanceof Error ? exc.message : String(exc)}`);
9633
+ }
9858
9634
  }
9859
- if (method === "message.ack") {
9635
+ if (method === "message.v2.ack" || method === "message.ack" && hasMatchingV2Session(client)) {
9860
9636
  await client._ensureV2SessionReady("message.ack");
9861
- client._clientLog.debug("call route: message.ack -> V2 ack");
9637
+ client._clientLog.debug(`call route: ${method} -> V2 ack`);
9862
9638
  return await client._ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || void 0);
9863
9639
  }
9864
- if (method === "group.pull" && p.group_id) {
9640
+ if ((method === "group.v2.pull" || method === "group.pull") && p.group_id && (method === "group.v2.pull" || hasMatchingV2Session(client))) {
9865
9641
  await client._ensureV2SessionReady("group.pull");
9866
- client._clientLog.debug("call route: group.pull -> V2 pull");
9642
+ client._clientLog.debug(`call route: ${method} -> V2 pull`);
9867
9643
  const hasExplicitAfterSeq = "after_seq" in p || "after_message_seq" in p;
9868
9644
  const cursorParams = client._explicitGroupCursorParams(p);
9869
9645
  const ownsCursor = Object.keys(cursorParams).length === 0 || client._groupCursorTargetsCurrentInstance(cursorParams);
@@ -9871,24 +9647,25 @@ var RpcPipeline = class {
9871
9647
  if (hasExplicitAfterSeq) pullOpts.explicitAfterSeq = true;
9872
9648
  if (Object.keys(cursorParams).length > 0) pullOpts.cursorParams = cursorParams;
9873
9649
  if (!ownsCursor) pullOpts.ownsCursor = false;
9650
+ if (String(p.group_id ?? "").trim()) pullOpts.wireGroupId = String(p.group_id).trim();
9874
9651
  const messages = await client._pullGroupV2(
9875
- String(p.group_id),
9652
+ String(p.group_aid ?? p.group_id),
9876
9653
  Number(p.after_seq ?? p.after_message_seq ?? 0) || 0,
9877
9654
  Number(p.limit ?? 50) || 50,
9878
9655
  Object.keys(pullOpts).length > 0 ? pullOpts : void 0
9879
9656
  );
9880
9657
  return { messages };
9881
9658
  }
9882
- if (method === "group.ack_messages" && p.group_id) {
9659
+ if ((method === "group.v2.ack" || method === "group.ack_messages") && p.group_id && (method === "group.v2.ack" || hasMatchingV2Session(client))) {
9883
9660
  await client._ensureV2SessionReady("group.ack_messages");
9884
- client._clientLog.debug("call route: group.ack_messages -> V2 ack");
9661
+ client._clientLog.debug(`call route: ${method} -> V2 ack`);
9885
9662
  const cursorParams = client._explicitGroupCursorParams(p);
9886
9663
  const ownsCursor = Object.keys(cursorParams).length === 0 || client._groupCursorTargetsCurrentInstance(cursorParams);
9887
9664
  if (!ownsCursor) {
9888
9665
  return await client._rawGroupAckMessages(p);
9889
9666
  }
9890
9667
  return await client._ackGroupV2(
9891
- String(p.group_id),
9668
+ String(p.group_aid ?? p.group_id),
9892
9669
  Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || void 0
9893
9670
  );
9894
9671
  }
@@ -9909,15 +9686,12 @@ var RpcPipeline = class {
9909
9686
  }
9910
9687
  preflight(method, params2) {
9911
9688
  const client = this.runtime.client;
9912
- if (client._state !== "connected") {
9689
+ if (client.state !== "ready" /* READY */) {
9913
9690
  throw new ConnectionError("client is not connected");
9914
9691
  }
9915
9692
  if (INTERNAL_ONLY_METHODS.has(method)) {
9916
9693
  throw new PermissionError(`method is internal_only: ${method}`);
9917
9694
  }
9918
- if (method.startsWith("message.e2ee.") || method.startsWith("group.e2ee.") || REMOVED_E2EE_METHODS.has(method)) {
9919
- throw new PermissionError(`legacy E2EE method is removed in this SDK: ${method}`);
9920
- }
9921
9695
  const p = { ...params2 ?? {} };
9922
9696
  this.mergeInstanceProtectedHeaders(method, p);
9923
9697
  const rpcBackground = Boolean(p._rpc_background) || client._backgroundRpcDepth > 0;
@@ -9925,6 +9699,7 @@ var RpcPipeline = class {
9925
9699
  if (method === "message.send" || method === "group.send") {
9926
9700
  this.normalizeOutboundMessagePayload(p, method);
9927
9701
  }
9702
+ this.normalizeGroupCallIdentifier(method, p);
9928
9703
  this.validateOutboundCall(method, p);
9929
9704
  this.injectMessageCursorContext(method, p);
9930
9705
  this.captureGroupCursorParams(method, p);
@@ -9954,6 +9729,7 @@ var RpcPipeline = class {
9954
9729
  validateOutboundCall(method, params2) {
9955
9730
  if (method === "message.send") {
9956
9731
  this.validateMessageRecipient(params2.to);
9732
+ validateAIDFormat(params2.to, "message.send.to");
9957
9733
  if ("persist" in params2) {
9958
9734
  throw new ValidationError("message.send no longer accepts 'persist'; configure delivery_mode during connect");
9959
9735
  }
@@ -9962,6 +9738,7 @@ var RpcPipeline = class {
9962
9738
  }
9963
9739
  }
9964
9740
  if (method === "group.send") {
9741
+ validateGroupIDFormat(params2.group_id, "group.send.group_id");
9965
9742
  if ("persist" in params2) {
9966
9743
  throw new ValidationError("group.send does not accept 'persist'; group messages are always fanout");
9967
9744
  }
@@ -9978,17 +9755,28 @@ var RpcPipeline = class {
9978
9755
  throw new ValidationError(`${method} requires context.type + context.id`);
9979
9756
  }
9980
9757
  }
9981
- if (method === "group.thought.get" && !String(params2.sender_aid ?? "").trim()) {
9982
- throw new ValidationError("group.thought.get requires sender_aid");
9758
+ if (method === "group.thought.put") {
9759
+ validateGroupIDFormat(params2.group_id, "group.thought.put.group_id");
9760
+ }
9761
+ if (method === "group.thought.get") {
9762
+ if (!String(params2.sender_aid ?? "").trim()) {
9763
+ throw new ValidationError("group.thought.get requires sender_aid");
9764
+ }
9765
+ validateAIDFormat(params2.sender_aid, "group.thought.get.sender_aid");
9766
+ validateGroupIDFormat(params2.group_id, "group.thought.get.group_id");
9983
9767
  }
9984
9768
  if (method === "message.thought.put") {
9985
9769
  this.validateMessageRecipient(params2.to);
9986
9770
  if (!String(params2.to ?? "").trim()) {
9987
9771
  throw new ValidationError("message.thought.put requires to");
9988
9772
  }
9773
+ validateAIDFormat(params2.to, "message.thought.put.to");
9989
9774
  }
9990
- if (method === "message.thought.get" && !String(params2.sender_aid ?? "").trim()) {
9991
- throw new ValidationError("message.thought.get requires sender_aid");
9775
+ if (method === "message.thought.get") {
9776
+ if (!String(params2.sender_aid ?? "").trim()) {
9777
+ throw new ValidationError("message.thought.get requires sender_aid");
9778
+ }
9779
+ validateAIDFormat(params2.sender_aid, "message.thought.get.sender_aid");
9992
9780
  }
9993
9781
  }
9994
9782
  injectMessageCursorContext(method, params2) {
@@ -10045,14 +9833,7 @@ var RpcPipeline = class {
10045
9833
  );
10046
9834
  const paramsHash = Array.from(new Uint8Array(paramsHashBuf)).map((b) => b.toString(16).padStart(2, "0")).join("");
10047
9835
  const signData = new TextEncoder().encode(`${method}|${aid}|${ts}|${paramsHash}`);
10048
- const pkcs8 = pemToArrayBuffer(currentAid.privateKeyPem);
10049
- const cryptoKey = await crypto.subtle.importKey(
10050
- "pkcs8",
10051
- pkcs8,
10052
- { name: "ECDSA", namedCurve: "P-256" },
10053
- false,
10054
- ["sign"]
10055
- );
9836
+ const cryptoKey = await signingPrivateKey(currentAid.privateKeyPem);
10056
9837
  const sigP1363 = await crypto.subtle.sign(
10057
9838
  { name: "ECDSA", hash: "SHA-256" },
10058
9839
  cryptoKey,
@@ -10062,9 +9843,7 @@ var RpcPipeline = class {
10062
9843
  let certFingerprint = "";
10063
9844
  const certPem = currentAid.certPem;
10064
9845
  if (certPem) {
10065
- const certDer = pemToArrayBuffer(certPem);
10066
- const fpBuf = await crypto.subtle.digest("SHA-256", certDer);
10067
- certFingerprint = "sha256:" + Array.from(new Uint8Array(fpBuf)).map((b) => b.toString(16).padStart(2, "0")).join("");
9846
+ certFingerprint = await signingCertFingerprint(certPem);
10068
9847
  }
10069
9848
  params2.client_signature = {
10070
9849
  aid,
@@ -10084,11 +9863,11 @@ var RpcPipeline = class {
10084
9863
  return client._aid ? `p2p:${client._aid}` : "";
10085
9864
  }
10086
9865
  if (method === "group.pull" || method === "group.v2.pull") {
10087
- const gid = String(params2.group_id ?? "").trim();
9866
+ const gid = String(params2.group_aid ?? params2.group_id ?? "").trim();
10088
9867
  return gid ? `group:${gid}` : "";
10089
9868
  }
10090
9869
  if (method === "group.pull_events") {
10091
- const gid = String(params2.group_id ?? "").trim();
9870
+ const gid = String(params2.group_aid ?? params2.group_id ?? "").trim();
10092
9871
  return gid ? `group_event:${gid}` : "";
10093
9872
  }
10094
9873
  return "";
@@ -10200,13 +9979,13 @@ var RpcPipeline = class {
10200
9979
  }
10201
9980
  }
10202
9981
  if (client._seqTracker.getContiguousSeq(ns) !== contigBefore) {
10203
- client._saveSeqTrackerState?.();
9982
+ client._persistSeq?.(ns);
10204
9983
  }
10205
9984
  result._contig_before = contigBefore;
10206
9985
  }
10207
9986
  postprocessGroupPull(params2, result) {
10208
9987
  const client = this.runtime.client;
10209
- const gid = String(params2.group_id ?? "").trim();
9988
+ const gid = String(params2.group_aid ?? params2.group_id ?? "").trim();
10210
9989
  if (!gid || !client._seqTracker) {
10211
9990
  return;
10212
9991
  }
@@ -10227,7 +10006,7 @@ var RpcPipeline = class {
10227
10006
  }
10228
10007
  }
10229
10008
  if (client._seqTracker.getContiguousSeq(ns) !== contigBefore) {
10230
- client._saveSeqTrackerState?.();
10009
+ client._persistSeq?.(ns);
10231
10010
  }
10232
10011
  result._contig_before = contigBefore;
10233
10012
  }
@@ -10245,14 +10024,7 @@ var RpcPipeline = class {
10245
10024
  return;
10246
10025
  }
10247
10026
  const client = this.runtime.client;
10248
- if (params2.group_id !== void 0 && params2.group_id !== null) {
10249
- const rawGroupId = String(params2.group_id);
10250
- const normalizedGroupId2 = normalizeGroupId(rawGroupId);
10251
- if (normalizedGroupId2 && normalizedGroupId2 !== rawGroupId) {
10252
- client._clientLog?.debug?.(`call group_id normalized: ${rawGroupId} -> ${normalizedGroupId2} method=${method}`);
10253
- }
10254
- params2.group_id = normalizedGroupId2;
10255
- }
10027
+ this.normalizeGroupCallIdentifier(method, params2);
10256
10028
  if (params2.device_id === void 0) {
10257
10029
  params2.device_id = client._deviceId;
10258
10030
  }
@@ -10260,6 +10032,40 @@ var RpcPipeline = class {
10260
10032
  params2.slot_id = client._slotId;
10261
10033
  }
10262
10034
  }
10035
+ normalizeGroupCallIdentifier(method, params2) {
10036
+ if (!method.startsWith("group.")) {
10037
+ return;
10038
+ }
10039
+ const record = params2;
10040
+ const rawEntries = [
10041
+ ["group_aid", record.group_aid],
10042
+ ["groupAid", record.groupAid],
10043
+ ["group_id", record.group_id],
10044
+ ["groupId", record.groupId]
10045
+ ];
10046
+ const found = rawEntries.find(([, value]) => String(value ?? "").trim());
10047
+ if (found) {
10048
+ const [key, raw] = found;
10049
+ const rawGroupId = String(raw).trim();
10050
+ const normalizedGroupId2 = normalizeGroupId(rawGroupId);
10051
+ if (normalizedGroupId2) {
10052
+ if (normalizedGroupId2 !== rawGroupId) {
10053
+ this.runtime.client._clientLog?.debug?.(`call group identifier normalized: ${key}=${rawGroupId} -> ${normalizedGroupId2} method=${method}`);
10054
+ }
10055
+ const existingGroupId = String(record.group_id ?? record.groupId ?? "").trim();
10056
+ if (key === "group_id" || key === "groupId") {
10057
+ params2.group_id = rawGroupId;
10058
+ } else if (existingGroupId) {
10059
+ params2.group_id = existingGroupId;
10060
+ } else {
10061
+ params2.group_id = normalizedGroupId2;
10062
+ }
10063
+ params2.group_aid = normalizedGroupId2;
10064
+ }
10065
+ }
10066
+ delete record.groupAid;
10067
+ delete record.groupId;
10068
+ }
10263
10069
  groupCursorParams(params2) {
10264
10070
  const cursorParams = {};
10265
10071
  for (const key of ["device_id", "slot_id", "device_name", "device_type"]) {
@@ -10696,6 +10502,15 @@ var StorageLowLevel = class {
10696
10502
  parents: options.parents ?? false
10697
10503
  }), options.path);
10698
10504
  }
10505
+ fsTouch(options) {
10506
+ return this.call("storage.fs.touch", params(options.owner, options.bucket, {
10507
+ path: options.path,
10508
+ parents: options.parents ?? false,
10509
+ no_create: options.noCreate ?? false,
10510
+ mtime: options.mtime,
10511
+ follow_symlinks: options.followSymlinks ?? false
10512
+ }), options.path);
10513
+ }
10699
10514
  fsRemove(options) {
10700
10515
  return this.call("storage.fs.remove", params(options.owner, options.bucket, {
10701
10516
  path: options.path,
@@ -10768,8 +10583,10 @@ var StorageLowLevel = class {
10768
10583
  }), options.mountPath);
10769
10584
  }
10770
10585
  fsInvalidateMembership(options) {
10586
+ const groupId = options.groupId ?? options.groupAid;
10771
10587
  return this.call("storage.fs.invalidate_membership", stripUndefined({
10772
- group_id: options.groupId,
10588
+ group_id: groupId,
10589
+ group_aid: options.groupAid,
10773
10590
  group_owner_aid: options.groupOwnerAid,
10774
10591
  member_aid: options.memberAid,
10775
10592
  reason: options.reason ?? "membership_changed",
@@ -11021,6 +10838,24 @@ var GroupFSVFS = class {
11021
10838
  mkdir(path, options = {}) {
11022
10839
  return this.call("group.fs.mkdir", { path, parents: options.parents ?? false, ...options }, path);
11023
10840
  }
10841
+ setAcl(path, options = {}) {
10842
+ const grantee = options.grantee_aid ?? options.granteeAid ?? "role:admin";
10843
+ const params2 = { ...options, path, grantee_aid: grantee, perms: options.perms ?? "rwx" };
10844
+ delete params2.granteeAid;
10845
+ return this.call("group.fs.set_acl", params2, path);
10846
+ }
10847
+ removeAcl(path, options = {}) {
10848
+ const grantee = options.grantee_aid ?? options.granteeAid ?? "role:admin";
10849
+ const params2 = { ...options, path, grantee_aid: grantee };
10850
+ delete params2.granteeAid;
10851
+ return this.call("group.fs.remove_acl", params2, path);
10852
+ }
10853
+ getAcl(path, options = {}) {
10854
+ return this.call("group.fs.get_acl", { path, ...options }, path);
10855
+ }
10856
+ listAcl(path, options = {}) {
10857
+ return this.call("group.fs.list_acl", { path, ...options }, path);
10858
+ }
11024
10859
  rm(path, options = {}) {
11025
10860
  return this.call("group.fs.rm", {
11026
10861
  path,
@@ -11030,21 +10865,21 @@ var GroupFSVFS = class {
11030
10865
  }, path);
11031
10866
  }
11032
10867
  async cp(src, dst, options = {}) {
11033
- const groupId = options.group_id ?? options.groupId;
11034
- const srcGroupId = options.src_group_id ?? options.srcGroupId;
11035
- const dstGroupId = options.dst_group_id ?? options.dstGroupId;
11036
- const srcRemote = typeof src === "string" && isGroupRemoteCopyPath(src, srcGroupId, groupId);
11037
- const dstRemote = typeof dst === "string" && isGroupRemoteCopyPath(dst, dstGroupId, groupId);
10868
+ const groupRef = options.group_aid ?? options.groupAid ?? options.group_id ?? options.groupId;
10869
+ const srcGroupRef = options.src_group_aid ?? options.srcGroupAid ?? options.src_group_id ?? options.srcGroupId;
10870
+ const dstGroupRef = options.dst_group_aid ?? options.dstGroupAid ?? options.dst_group_id ?? options.dstGroupId;
10871
+ const srcRemote = typeof src === "string" && isGroupRemoteCopyPath(src, srcGroupRef, groupRef);
10872
+ const dstRemote = typeof dst === "string" && isGroupRemoteCopyPath(dst, dstGroupRef, groupRef);
11038
10873
  if (srcRemote && dstRemote) return this.copyRemoteToRemote(src, dst, options);
11039
10874
  if (!srcRemote && dstRemote) return this.uploadToGroup(src, dst, options);
11040
10875
  if (srcRemote && !dstRemote) return this.downloadFromGroup(src, dst, options);
11041
10876
  throw new StorageError("local-to-local copy is not handled by group.fs", "EINVAL", typeof src === "string" ? src : "");
11042
10877
  }
11043
10878
  async mv(src, dst, options = {}) {
11044
- const groupId = options.group_id ?? options.groupId;
11045
- const srcGroupId = options.src_group_id ?? options.srcGroupId;
11046
- const dstGroupId = options.dst_group_id ?? options.dstGroupId;
11047
- if (!isGroupRemoteCopyPath(src, srcGroupId, groupId) || !isGroupRemoteCopyPath(dst, dstGroupId, groupId)) {
10879
+ const groupRef = options.group_aid ?? options.groupAid ?? options.group_id ?? options.groupId;
10880
+ const srcGroupRef = options.src_group_aid ?? options.srcGroupAid ?? options.src_group_id ?? options.srcGroupId;
10881
+ const dstGroupRef = options.dst_group_aid ?? options.dstGroupAid ?? options.dst_group_id ?? options.dstGroupId;
10882
+ if (!isGroupRemoteCopyPath(src, srcGroupRef, groupRef) || !isGroupRemoteCopyPath(dst, dstGroupRef, groupRef)) {
11048
10883
  throw new StorageError("group.fs.mv only supports group remote paths", "EINVAL", src);
11049
10884
  }
11050
10885
  const params2 = this.remoteCopyParams(src, dst, options);
@@ -11075,10 +10910,16 @@ var GroupFSVFS = class {
11075
10910
  delete params2.source_type;
11076
10911
  delete params2.localPath;
11077
10912
  delete params2.local_path;
10913
+ if (options.groupAid !== void 0) params2.group_aid = options.groupAid;
11078
10914
  if (options.groupId !== void 0) params2.group_id = options.groupId;
10915
+ if (options.srcGroupAid !== void 0) params2.src_group_aid = options.srcGroupAid;
11079
10916
  if (options.srcGroupId !== void 0) params2.src_group_id = options.srcGroupId;
10917
+ if (options.dstGroupAid !== void 0) params2.dst_group_aid = options.dstGroupAid;
11080
10918
  if (options.dstGroupId !== void 0) params2.dst_group_id = options.dstGroupId;
11081
10919
  if (options.followSymlinks !== void 0) params2.follow_symlinks = options.followSymlinks;
10920
+ delete params2.groupAid;
10921
+ delete params2.srcGroupAid;
10922
+ delete params2.dstGroupAid;
11082
10923
  delete params2.followSymlinks;
11083
10924
  if (!options.force) delete params2.force;
11084
10925
  if (!options.recursive) delete params2.recursive;
@@ -11104,10 +10945,13 @@ var GroupFSVFS = class {
11104
10945
  async uploadToGroup(src, dst, options) {
11105
10946
  const { data, contentType } = await this.sourceBytes(src, options);
11106
10947
  const digest = await sha256Hex(data);
10948
+ const groupAid = options.group_aid ?? options.groupAid ?? options.dst_group_aid ?? options.dstGroupAid;
10949
+ const groupId = options.group_id ?? options.groupId ?? options.dst_group_id ?? options.dstGroupId;
11107
10950
  const baseParams = stripNil({
11108
10951
  ...options,
11109
10952
  path: dst,
11110
- group_id: options.group_id ?? options.groupId ?? options.dst_group_id ?? options.dstGroupId,
10953
+ group_aid: groupAid,
10954
+ group_id: groupAid ? void 0 : groupId,
11111
10955
  size_bytes: data.byteLength,
11112
10956
  sha256: digest,
11113
10957
  content_type: options.content_type ?? options.contentType ?? contentType,
@@ -11117,9 +10961,14 @@ var GroupFSVFS = class {
11117
10961
  expected_version: options.expected_version ?? options.expectedVersion
11118
10962
  });
11119
10963
  delete baseParams.groupId;
10964
+ delete baseParams.groupAid;
11120
10965
  delete baseParams.dstGroupId;
10966
+ delete baseParams.dstGroupAid;
10967
+ delete baseParams.dst_group_aid;
11121
10968
  delete baseParams.dst_group_id;
11122
10969
  delete baseParams.srcGroupId;
10970
+ delete baseParams.srcGroupAid;
10971
+ delete baseParams.src_group_aid;
11123
10972
  delete baseParams.src_group_id;
11124
10973
  delete baseParams.contentType;
11125
10974
  delete baseParams.expectedVersion;
@@ -11177,15 +11026,23 @@ var GroupFSVFS = class {
11177
11026
  throw new StorageExistsError(`local path already exists: ${target.localPath}`, "EEXIST", target.localPath);
11178
11027
  }
11179
11028
  }
11029
+ const groupAid = options.group_aid ?? options.groupAid ?? options.src_group_aid ?? options.srcGroupAid;
11030
+ const groupId = options.group_id ?? options.groupId ?? options.src_group_id ?? options.srcGroupId;
11180
11031
  const params2 = stripNil({
11181
11032
  ...options,
11182
11033
  path: src,
11183
- group_id: options.group_id ?? options.groupId ?? options.src_group_id ?? options.srcGroupId
11034
+ group_aid: groupAid,
11035
+ group_id: groupAid ? void 0 : groupId
11184
11036
  });
11185
11037
  delete params2.groupId;
11038
+ delete params2.groupAid;
11186
11039
  delete params2.srcGroupId;
11040
+ delete params2.srcGroupAid;
11041
+ delete params2.src_group_aid;
11187
11042
  delete params2.src_group_id;
11188
11043
  delete params2.dstGroupId;
11044
+ delete params2.dstGroupAid;
11045
+ delete params2.dst_group_aid;
11189
11046
  delete params2.dst_group_id;
11190
11047
  delete params2.force;
11191
11048
  delete params2.recursive;
@@ -11249,6 +11106,20 @@ function stripNil2(params2) {
11249
11106
  }
11250
11107
  return out;
11251
11108
  }
11109
+ function splitAidStore(params2) {
11110
+ const out = stripNil2(params2);
11111
+ const aidStore = out.aidStore ?? out.aid_store;
11112
+ delete out.aidStore;
11113
+ delete out.aid_store;
11114
+ return { params: out, aidStore };
11115
+ }
11116
+ function settingsToMap(result) {
11117
+ const settings = {};
11118
+ for (const s of result.settings || []) {
11119
+ settings[s.key] = s.value;
11120
+ }
11121
+ return settings;
11122
+ }
11252
11123
  var RpcFacade = class {
11253
11124
  constructor(client) {
11254
11125
  __publicField(this, "client");
@@ -11314,13 +11185,20 @@ var GroupFacade = class extends RpcFacade {
11314
11185
  return this._thought;
11315
11186
  }
11316
11187
  create(params2) {
11317
- return this.call("group.create", params2);
11188
+ const clean2 = stripNil2(params2);
11189
+ if (clean2.group_name && typeof this.client.createGroup === "function") {
11190
+ return this.client.createGroup(clean2);
11191
+ }
11192
+ return this.call("group.create", clean2);
11318
11193
  }
11319
11194
  bindAid(params2) {
11320
11195
  return this.call("group.bind_aid", params2);
11321
11196
  }
11322
- get(params2) {
11323
- return this.call("group.get", params2);
11197
+ bindGroupAid(params2) {
11198
+ return this.call("group.bind_group_aid", params2);
11199
+ }
11200
+ renewGroupAid(params2) {
11201
+ return this.call("group.renew_group_aid", params2);
11324
11202
  }
11325
11203
  getInfo(params2) {
11326
11204
  return this.call("group.get_info", params2);
@@ -11337,9 +11215,6 @@ var GroupFacade = class extends RpcFacade {
11337
11215
  search(params2) {
11338
11216
  return this.call("group.search", params2);
11339
11217
  }
11340
- getPublicInfo(params2) {
11341
- return this.call("group.get_public_info", params2);
11342
- }
11343
11218
  suspend(params2) {
11344
11219
  return this.call("group.suspend", params2);
11345
11220
  }
@@ -11349,9 +11224,6 @@ var GroupFacade = class extends RpcFacade {
11349
11224
  dissolve(params2) {
11350
11225
  return this.call("group.dissolve", params2);
11351
11226
  }
11352
- getStats(params2) {
11353
- return this.call("group.get_stats", params2);
11354
- }
11355
11227
  addMember(params2) {
11356
11228
  return this.call("group.add_member", params2);
11357
11229
  }
@@ -11368,10 +11240,18 @@ var GroupFacade = class extends RpcFacade {
11368
11240
  return this.call("group.set_role", params2);
11369
11241
  }
11370
11242
  transferOwner(params2) {
11371
- return this.call("group.transfer_owner", params2);
11243
+ const split = splitAidStore(params2);
11244
+ if (split.aidStore && typeof this.client.startGroupTransfer === "function") {
11245
+ return this.client.startGroupTransfer(split.params, { aidStore: split.aidStore });
11246
+ }
11247
+ return this.call("group.transfer_owner", split.params);
11372
11248
  }
11373
11249
  completeTransfer(params2) {
11374
- return this.call("group.complete_transfer", params2);
11250
+ const split = splitAidStore(params2);
11251
+ if (split.aidStore && typeof this.client.completeGroupTransfer === "function") {
11252
+ return this.client.completeGroupTransfer(split.params, { aidStore: split.aidStore });
11253
+ }
11254
+ return this.call("group.complete_transfer", split.params);
11375
11255
  }
11376
11256
  ban(params2) {
11377
11257
  return this.call("group.ban", params2);
@@ -11413,12 +11293,14 @@ var GroupFacade = class extends RpcFacade {
11413
11293
  return this.call("group.get_settings", params2);
11414
11294
  }
11415
11295
  send(params2) {
11296
+ validateGroupIDFormat(params2?.group_id, "group_id");
11416
11297
  return this.call("group.send", params2);
11417
11298
  }
11418
11299
  recall(params2) {
11419
11300
  return this.call("group.recall", params2);
11420
11301
  }
11421
11302
  pull(params2) {
11303
+ validateGroupIDFormat(params2?.group_id, "group_id");
11422
11304
  return this.call("group.pull", params2);
11423
11305
  }
11424
11306
  pullEvents(params2) {
@@ -11430,30 +11312,146 @@ var GroupFacade = class extends RpcFacade {
11430
11312
  ackEvents(params2) {
11431
11313
  return this.call("group.ack_events", params2);
11432
11314
  }
11433
- getAnnouncement(params2) {
11434
- return this.call("group.get_announcement", params2);
11315
+ async getAnnouncement(params2) {
11316
+ const merged = params2 || {};
11317
+ const groupId = merged.group_id;
11318
+ if (!groupId) throw new Error("group_id is required");
11319
+ const result = await this.getSettings({
11320
+ group_id: groupId,
11321
+ keys: ["announcement.content", "announcement.attachments"]
11322
+ });
11323
+ const settings = settingsToMap(result);
11324
+ return {
11325
+ group_id: result.group_id,
11326
+ announcement: {
11327
+ group_id: result.group_id,
11328
+ content: settings["announcement.content"] || "",
11329
+ attachments: settings["announcement.attachments"] || [],
11330
+ updated_by: settings["announcement.content.updated_by"] || "",
11331
+ updated_at: settings["announcement.content.updated_at"] || 0
11332
+ }
11333
+ };
11435
11334
  }
11436
- updateAnnouncement(params2) {
11437
- return this.call("group.update_announcement", params2);
11335
+ async updateAnnouncement(params2) {
11336
+ const merged = params2 || {};
11337
+ const groupId = merged.group_id;
11338
+ const content = merged.content;
11339
+ const attachments = merged.attachments;
11340
+ if (!groupId) throw new Error("group_id is required");
11341
+ if (content === void 0) throw new Error("content is required");
11342
+ const settingsUpdate = { "announcement.content": content };
11343
+ if (attachments !== void 0) {
11344
+ settingsUpdate["announcement.attachments"] = attachments;
11345
+ }
11346
+ const result = await this.setSettings({
11347
+ group_id: groupId,
11348
+ settings: settingsUpdate
11349
+ });
11350
+ return {
11351
+ group_id: result.group_id,
11352
+ announcement: {
11353
+ group_id: result.group_id,
11354
+ content,
11355
+ attachments: attachments || []
11356
+ }
11357
+ };
11438
11358
  }
11439
- getRules(params2) {
11440
- return this.call("group.get_rules", params2);
11359
+ async getRules(params2) {
11360
+ const merged = params2 || {};
11361
+ const groupId = merged.group_id;
11362
+ if (!groupId) throw new Error("group_id is required");
11363
+ const result = await this.getSettings({
11364
+ group_id: groupId,
11365
+ keys: ["rules.content", "rules.attachments"]
11366
+ });
11367
+ const settings = settingsToMap(result);
11368
+ return {
11369
+ group_id: result.group_id,
11370
+ rules: {
11371
+ group_id: result.group_id,
11372
+ content: settings["rules.content"] || "",
11373
+ attachments: settings["rules.attachments"] || [],
11374
+ updated_by: settings["rules.content.updated_by"] || "",
11375
+ updated_at: settings["rules.content.updated_at"] || 0
11376
+ }
11377
+ };
11441
11378
  }
11442
- updateRules(params2) {
11443
- return this.call("group.update_rules", params2);
11379
+ async updateRules(params2) {
11380
+ const merged = params2 || {};
11381
+ const groupId = merged.group_id;
11382
+ const content = merged.content;
11383
+ const attachments = merged.attachments;
11384
+ if (!groupId) throw new Error("group_id is required");
11385
+ if (content === void 0) throw new Error("content is required");
11386
+ const settingsUpdate = { "rules.content": content };
11387
+ if (attachments !== void 0) {
11388
+ settingsUpdate["rules.attachments"] = attachments;
11389
+ }
11390
+ const result = await this.setSettings({
11391
+ group_id: groupId,
11392
+ settings: settingsUpdate
11393
+ });
11394
+ return {
11395
+ group_id: result.group_id,
11396
+ rules: {
11397
+ group_id: result.group_id,
11398
+ content,
11399
+ attachments: attachments || []
11400
+ }
11401
+ };
11444
11402
  }
11445
- getJoinRequirements(params2) {
11446
- return this.call("group.get_join_requirements", params2);
11403
+ async getJoinRequirements(params2) {
11404
+ const merged = params2 || {};
11405
+ const groupId = merged.group_id;
11406
+ if (!groupId) throw new Error("group_id is required");
11407
+ const result = await this.getSettings({
11408
+ group_id: groupId,
11409
+ keys: ["join.mode", "join.question", "join.auto_approve_patterns", "join.max_pending"]
11410
+ });
11411
+ const settings = settingsToMap(result);
11412
+ return {
11413
+ group_id: result.group_id,
11414
+ join_requirements: {
11415
+ group_id: result.group_id,
11416
+ mode: settings["join.mode"] || "open",
11417
+ question: settings["join.question"] || "",
11418
+ auto_approve_patterns: settings["join.auto_approve_patterns"] || [],
11419
+ max_pending: settings["join.max_pending"] || 100,
11420
+ updated_by: settings["join.mode.updated_by"] || "",
11421
+ updated_at: settings["join.mode.updated_at"] || 0
11422
+ }
11423
+ };
11447
11424
  }
11448
- updateJoinRequirements(params2) {
11449
- return this.call("group.update_join_requirements", params2);
11425
+ async updateJoinRequirements(params2) {
11426
+ const merged = params2 || {};
11427
+ const groupId = merged.group_id;
11428
+ if (!groupId) throw new Error("group_id is required");
11429
+ const settingsUpdate = {};
11430
+ if ("mode" in merged) settingsUpdate["join.mode"] = merged.mode;
11431
+ if ("question" in merged) settingsUpdate["join.question"] = merged.question;
11432
+ if ("auto_approve_patterns" in merged) settingsUpdate["join.auto_approve_patterns"] = merged.auto_approve_patterns;
11433
+ if ("max_pending" in merged) settingsUpdate["join.max_pending"] = merged.max_pending;
11434
+ if (Object.keys(settingsUpdate).length === 0) {
11435
+ throw new Error("at least one field to update is required");
11436
+ }
11437
+ const result = await this.setSettings({
11438
+ group_id: groupId,
11439
+ settings: settingsUpdate
11440
+ });
11441
+ return {
11442
+ group_id: result.group_id,
11443
+ join_requirements: {
11444
+ group_id: result.group_id,
11445
+ mode: merged.mode,
11446
+ question: merged.question,
11447
+ auto_approve_patterns: merged.auto_approve_patterns,
11448
+ max_pending: merged.max_pending
11449
+ }
11450
+ };
11450
11451
  }
11451
11452
  getOnlineMembers(params2) {
11452
11453
  return this.call("group.get_online_members", params2);
11453
11454
  }
11454
- info(params2) {
11455
- return this.call("group.info", params2);
11456
- }
11457
11455
  };
11458
11456
  var StreamFacade = class extends RpcFacade {
11459
11457
  create(params2) {
@@ -11782,6 +11780,18 @@ var StorageVFS = class {
11782
11780
  const raw = await this.lowlevel.fsMkdir({ owner: this.owner(options.owner), bucket: options.bucket ?? "default", path: pathToKey(path), parents: options.parents });
11783
11781
  return nodeFromAny(raw.node ?? raw);
11784
11782
  }
11783
+ async touch(path, options = {}) {
11784
+ const raw = await this.lowlevel.fsTouch({
11785
+ owner: this.owner(options.owner),
11786
+ bucket: options.bucket ?? "default",
11787
+ path: pathToKey(path),
11788
+ parents: options.parents,
11789
+ noCreate: options.noCreate,
11790
+ mtime: options.mtime,
11791
+ followSymlinks: options.followSymlinks
11792
+ });
11793
+ return nodeFromAny(raw.node ?? raw);
11794
+ }
11785
11795
  async remove(path, options = {}) {
11786
11796
  const raw = await this.lowlevel.fsRemove({ owner: this.owner(options.owner), bucket: options.bucket ?? "default", path: pathToKey(path), recursive: options.recursive });
11787
11797
  return { path, removedCount: Number(raw.removed_count ?? raw.deleted_count ?? 0) };
@@ -11825,6 +11835,46 @@ var StorageVFS = class {
11825
11835
  const raw = await this.lowlevel.fsDf({ owner, bucket: options.bucket ?? "default" });
11826
11836
  return usageFromAny(raw, owner ?? void 0);
11827
11837
  }
11838
+ async du(path, options = {}) {
11839
+ const nodes = await this.find(path, {
11840
+ owner: options.owner,
11841
+ bucket: options.bucket,
11842
+ page: 1,
11843
+ pageSize: options.pageSize ?? 1e3,
11844
+ token: options.token
11845
+ });
11846
+ const rootParts = pathToKey(path).split("/").filter(Boolean);
11847
+ let sizeBytes = 0;
11848
+ let fileCount = 0;
11849
+ let dirCount = 0;
11850
+ let symlinkCount = 0;
11851
+ let truncated = false;
11852
+ for (const node of nodes) {
11853
+ const nodeParts = pathToKey(node.path).split("/").filter(Boolean);
11854
+ const depth = Math.max(0, nodeParts.length - rootParts.length);
11855
+ if (options.maxDepth !== void 0 && depth > options.maxDepth) {
11856
+ truncated = true;
11857
+ continue;
11858
+ }
11859
+ if (node.type === "file") {
11860
+ fileCount += 1;
11861
+ sizeBytes += node.size;
11862
+ } else if (node.type === "dir") {
11863
+ dirCount += 1;
11864
+ } else if (node.type === "symlink") {
11865
+ symlinkCount += 1;
11866
+ }
11867
+ }
11868
+ return {
11869
+ path,
11870
+ sizeBytes,
11871
+ fileCount,
11872
+ dirCount,
11873
+ symlinkCount,
11874
+ maxDepth: options.maxDepth,
11875
+ truncated
11876
+ };
11877
+ }
11828
11878
  async mount(source, mountPath, options = {}) {
11829
11879
  const owner = this.owner(options.owner);
11830
11880
  const mountRef = parsePathLike(mountPath, owner);
@@ -12137,6 +12187,18 @@ var CollabClient = class {
12137
12187
  unregister(groupAid, collabRoot) {
12138
12188
  return this._call("collab.unregister", { group_aid: groupAid, collab_root: collabRoot });
12139
12189
  }
12190
+ setAcl(collabRoot, granteeAid, options = {}) {
12191
+ return this._call("collab.set_acl", stripNil3({
12192
+ collab_root: collabRoot,
12193
+ grantee_aid: granteeAid,
12194
+ perms: options.perms ?? "w",
12195
+ expires_at: options.expires_at ?? options.expiresAt,
12196
+ max_uses: options.max_uses ?? options.maxUses
12197
+ }));
12198
+ }
12199
+ removeAcl(collabRoot, granteeAid) {
12200
+ return this._call("collab.remove_acl", { collab_root: collabRoot, grantee_aid: granteeAid });
12201
+ }
12140
12202
  };
12141
12203
 
12142
12204
  // src/client/v2-e2ee.ts
@@ -16051,10 +16113,22 @@ var V2Session = class {
16051
16113
  const hex = await sha256Hex3(this._ikPubDer);
16052
16114
  return `sha256:${hex.substring(0, 16)}`;
16053
16115
  }
16116
+ _groupKey(groupId) {
16117
+ return normalizeGroupId(groupId) || String(groupId ?? "").trim();
16118
+ }
16119
+ _groupLookupCandidates(groupId) {
16120
+ const raw = String(groupId ?? "").trim();
16121
+ const normalized = this._groupKey(raw);
16122
+ const out = [];
16123
+ for (const item of [normalized, raw]) {
16124
+ if (item && !out.includes(item)) out.push(item);
16125
+ }
16126
+ return out;
16127
+ }
16054
16128
  _normalizeGroupSPKLookup(groupId, spkId) {
16055
16129
  const nul = spkId.indexOf("\0");
16056
- if (nul < 0) return { groupId, spkId };
16057
- return { groupId: spkId.slice(0, nul).trim(), spkId: spkId.slice(nul + 1) };
16130
+ if (nul < 0) return { groupId: this._groupKey(groupId), spkId };
16131
+ return { groupId: this._groupKey(spkId.slice(0, nul)), spkId: spkId.slice(nul + 1) };
16058
16132
  }
16059
16133
  /** SPK 由 AID 私钥(IK)签名背书并上报到 message.v2.put_peer_pk。 */
16060
16134
  async _registerSPK(callFn) {
@@ -16155,10 +16229,13 @@ var V2Session = class {
16155
16229
  return null;
16156
16230
  }
16157
16231
  async _loadGroupSPK(groupId, spkId) {
16158
- const scoped = await this._store.loadGroupSPK(this._storeDeviceId, groupId, spkId);
16159
- if (scoped) return scoped;
16160
- if (this._storeDeviceId !== this._deviceId) {
16161
- return this._store.loadGroupSPK(this._deviceId, groupId, spkId);
16232
+ for (const candidate of this._groupLookupCandidates(groupId)) {
16233
+ const scoped = await this._store.loadGroupSPK(this._storeDeviceId, candidate, spkId);
16234
+ if (scoped) return scoped;
16235
+ if (this._storeDeviceId !== this._deviceId) {
16236
+ const legacy = await this._store.loadGroupSPK(this._deviceId, candidate, spkId);
16237
+ if (legacy) return legacy;
16238
+ }
16162
16239
  }
16163
16240
  return null;
16164
16241
  }
@@ -16245,7 +16322,7 @@ var V2Session = class {
16245
16322
  /** 判断 spkId 是否为本进程在该群最后一次成功上传的 group SPK。 */
16246
16323
  isLastUploadedGroupSPK(groupId, spkId) {
16247
16324
  if (!spkId) return false;
16248
- const gk = (groupId || "").trim();
16325
+ const gk = this._groupKey(groupId);
16249
16326
  const lookup = this._normalizeGroupSPKLookup(gk, spkId);
16250
16327
  return this._lastUploadedGroupSPKIds.get(lookup.groupId) === lookup.spkId;
16251
16328
  }
@@ -16253,9 +16330,15 @@ var V2Session = class {
16253
16330
  /** 确保指定群有独立 group SPK,返回 { spkId, priv, pubDer }。 */
16254
16331
  async ensureGroupSPK(groupId) {
16255
16332
  await this.ensureKeys();
16256
- const gk = (groupId || "").trim();
16257
- const existing = await this._store.loadCurrentGroupSPK(this._storeDeviceId, gk);
16258
- if (existing) return existing;
16333
+ const gk = this._groupKey(groupId);
16334
+ for (const candidate of this._groupLookupCandidates(groupId)) {
16335
+ const existing = await this._store.loadCurrentGroupSPK(this._storeDeviceId, candidate);
16336
+ if (existing) return existing;
16337
+ if (this._storeDeviceId !== this._deviceId) {
16338
+ const legacy = await this._store.loadCurrentGroupSPK(this._deviceId, candidate);
16339
+ if (legacy) return legacy;
16340
+ }
16341
+ }
16259
16342
  const [priv, pubDer] = await generateP256Keypair();
16260
16343
  const hex = await sha256Hex3(pubDer);
16261
16344
  const spkId = `sha256:${hex.substring(0, 16)}`;
@@ -16265,11 +16348,14 @@ var V2Session = class {
16265
16348
  /** 注册指定群的 group SPK。group 服务负责成员鉴权。 */
16266
16349
  async ensureGroupRegistered(groupId, callFn) {
16267
16350
  await this.ensureKeys();
16268
- const gk = (groupId || "").trim();
16269
- const uploadedSPKId = await this._store.loadLatestUploadedGroupSPKId(this._storeDeviceId, gk);
16270
- if (uploadedSPKId) {
16271
- this._lastUploadedGroupSPKIds.set(gk, uploadedSPKId);
16272
- return;
16351
+ const gk = this._groupKey(groupId);
16352
+ for (const candidate of this._groupLookupCandidates(groupId)) {
16353
+ const uploadedSPKId = await this._store.loadLatestUploadedGroupSPKId(this._storeDeviceId, candidate) || (this._storeDeviceId !== this._deviceId ? await this._store.loadLatestUploadedGroupSPKId(this._deviceId, candidate) : null);
16354
+ if (uploadedSPKId) {
16355
+ this._lastUploadedGroupSPKIds.set(gk, uploadedSPKId);
16356
+ this._lastUploadedGroupSPKIds.set(candidate, uploadedSPKId);
16357
+ return;
16358
+ }
16273
16359
  }
16274
16360
  const { spkId, pubDer } = await this.ensureGroupSPK(gk);
16275
16361
  await this._publishGroupSPK(gk, spkId, pubDer, callFn);
@@ -16277,7 +16363,7 @@ var V2Session = class {
16277
16363
  /** 轮换指定群的 group SPK,保留旧私钥用于缓存窗口内的历史 wrap 解密。 */
16278
16364
  async rotateGroupSPK(groupId, callFn) {
16279
16365
  await this.ensureKeys();
16280
- const gk = (groupId || "").trim();
16366
+ const gk = this._groupKey(groupId);
16281
16367
  const [priv, pubDer] = await generateP256Keypair();
16282
16368
  const hex = await sha256Hex3(pubDer);
16283
16369
  const spkId = `sha256:${hex.substring(0, 16)}`;
@@ -16288,7 +16374,7 @@ var V2Session = class {
16288
16374
  /** 群消息解密按 group SPK -> device SPK -> IK fallback;仍找不到则显式报错。 */
16289
16375
  async getGroupDecryptKeys(groupId, spkId) {
16290
16376
  await this.ensureKeys();
16291
- const gk = (groupId || "").trim();
16377
+ const gk = this._groupKey(groupId);
16292
16378
  if (!spkId) return { ikPriv: this._ikPriv };
16293
16379
  const lookup = this._normalizeGroupSPKLookup(gk, spkId);
16294
16380
  const groupSpk = await this._loadGroupSPK(lookup.groupId, lookup.spkId);
@@ -16296,6 +16382,7 @@ var V2Session = class {
16296
16382
  return this.getDecryptKeys(lookup.spkId);
16297
16383
  }
16298
16384
  async _publishGroupSPK(groupId, spkId, spkPubDer, callFn) {
16385
+ const gk = this._groupKey(groupId);
16299
16386
  const spkTimestamp = Math.floor(this._nowFn() / 1e3);
16300
16387
  const enc = new TextEncoder();
16301
16388
  const signData = concatBytes3(
@@ -16305,15 +16392,19 @@ var V2Session = class {
16305
16392
  );
16306
16393
  const signature = await ecdsaSignRaw(this._ikPriv, signData);
16307
16394
  await callFn("group.v2.put_group_pk", {
16308
- group_id: groupId,
16395
+ group_id: gk,
16396
+ group_aid: gk,
16309
16397
  key_source: "group_device_prekey",
16310
16398
  spk_id: spkId,
16311
16399
  spk_pk: bytesToBase645(spkPubDer),
16312
16400
  spk_signature: bytesToBase645(signature),
16313
16401
  spk_timestamp: spkTimestamp
16314
16402
  });
16315
- await this._store.markGroupSPKUploaded(this._storeDeviceId, groupId, spkId);
16316
- this._lastUploadedGroupSPKIds.set(groupId, spkId);
16403
+ await this._store.markGroupSPKUploaded(this._storeDeviceId, gk, spkId);
16404
+ this._lastUploadedGroupSPKIds.set(gk, spkId);
16405
+ if (String(groupId ?? "").trim() && String(groupId ?? "").trim() !== gk) {
16406
+ this._lastUploadedGroupSPKIds.set(String(groupId ?? "").trim(), spkId);
16407
+ }
16317
16408
  }
16318
16409
  cachePeerIK(peerAid, deviceId, ikPubDer) {
16319
16410
  this._peerIKCache.set(`${peerAid}#${deviceId}`, {
@@ -16342,6 +16433,10 @@ var V2Session = class {
16342
16433
  // src/client/v2-e2ee.ts
16343
16434
  var V2_BOOTSTRAP_TTL_MS = 60 * 60 * 1e3;
16344
16435
  var V2_RETRYABLE_CODES = /* @__PURE__ */ new Set([-33011, -33012, -33050, -33052, -33054]);
16436
+ var V2_GROUP_STALE_BOOTSTRAP_CODE = -33054;
16437
+ var V2_SESSION_INIT_RETRY_LIMIT = 12;
16438
+ var V2_SESSION_INIT_RETRY_BASE_MS = 1e3;
16439
+ var V2_SESSION_INIT_RETRY_MAX_MS = 5e3;
16345
16440
  var MEMBERSHIP_ACTIONS = /* @__PURE__ */ new Set([
16346
16441
  "member_added",
16347
16442
  "member_left",
@@ -16353,6 +16448,17 @@ var MEMBERSHIP_ACTIONS = /* @__PURE__ */ new Set([
16353
16448
  "invite_code_used"
16354
16449
  ]);
16355
16450
  var JOIN_ACTIONS = /* @__PURE__ */ new Set(["member_added", "joined", "join_approved", "invite_code_used"]);
16451
+ function v2PullTailDelayMs(msgCount, pageLimit) {
16452
+ const count = Math.trunc(Number(msgCount || 0));
16453
+ const limit = Math.trunc(Number(pageLimit || 0));
16454
+ if (count <= 0 || limit > 0 && count >= limit) return 0;
16455
+ return Math.min(500, Math.max(5, 1e3 / (count + 1)));
16456
+ }
16457
+ function v2Sleep(ms) {
16458
+ return new Promise((resolve) => {
16459
+ globalThis.setTimeout(resolve, ms);
16460
+ });
16461
+ }
16356
16462
  function exactArrayBuffer(bytes) {
16357
16463
  return bytes.slice().buffer;
16358
16464
  }
@@ -16387,6 +16493,10 @@ function v2B64uToBytes(value) {
16387
16493
  function formatE2EEError(error) {
16388
16494
  return error instanceof Error ? error : String(error);
16389
16495
  }
16496
+ function isTransientV2SessionInitError(error) {
16497
+ const text3 = (error instanceof Error ? error.message : String(error)).toLowerCase();
16498
+ return text3.includes("no_service") || text3.includes("service plane unavailable") || text3.includes("transport not connected") || text3.includes("transport closed") || text3.includes("websocket closed") || text3.includes("websocket is not open") || text3.includes("connection closed") || text3.includes("socket closed");
16499
+ }
16390
16500
  function uuidV4() {
16391
16501
  if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
16392
16502
  const bytes = new Uint8Array(16);
@@ -16519,6 +16629,10 @@ function applyV2WrapPolicyToTargets(targets, policy) {
16519
16629
  var V2E2EECoordinator = class {
16520
16630
  constructor(runtime) {
16521
16631
  __publicField(this, "runtime");
16632
+ __publicField(this, "targetSetCache", /* @__PURE__ */ new Map());
16633
+ __publicField(this, "v2SessionInitRetryTimer", null);
16634
+ __publicField(this, "v2SessionInitRetryBackgroundSync", false);
16635
+ __publicField(this, "v2SessionInitRetryIdentityKey", null);
16522
16636
  this.runtime = runtime;
16523
16637
  }
16524
16638
  get client() {
@@ -16539,28 +16653,146 @@ var V2E2EECoordinator = class {
16539
16653
  }
16540
16654
  deleteBootstrapCacheEntry(key) {
16541
16655
  this.bootstrapCache.delete(key);
16656
+ this.targetSetCache.delete(key);
16542
16657
  }
16543
16658
  clearBootstrapCache() {
16544
16659
  this.bootstrapCache.clear();
16660
+ this.targetSetCache.clear();
16545
16661
  }
16546
16662
  pruneExpiredBootstrapCache(ttlMs, now = Date.now()) {
16547
16663
  for (const [key, entry] of this.bootstrapCache) {
16548
16664
  if (now - Number(entry.cachedAt ?? 0) >= ttlMs) {
16549
16665
  this.bootstrapCache.delete(key);
16666
+ this.targetSetCache.delete(key);
16667
+ }
16668
+ }
16669
+ for (const [key, entry] of this.targetSetCache) {
16670
+ if (now - Number(entry.cachedAt ?? 0) >= ttlMs) {
16671
+ this.targetSetCache.delete(key);
16550
16672
  }
16551
16673
  }
16552
16674
  }
16675
+ getTargetSetCache(key) {
16676
+ const cached = this.targetSetCache.get(key);
16677
+ if (!cached || Date.now() - cached.cachedAt >= V2_BOOTSTRAP_TTL_MS) {
16678
+ if (cached) this.targetSetCache.delete(key);
16679
+ return void 0;
16680
+ }
16681
+ return cached.value;
16682
+ }
16683
+ setTargetSetCache(key, value) {
16684
+ this.targetSetCache.set(key, { cachedAt: Date.now(), value });
16685
+ return value;
16686
+ }
16687
+ cloneP2PTargetSet(value) {
16688
+ return {
16689
+ targets: value.targets.map((target) => ({ ...target })),
16690
+ auditRecipients: value.auditRecipients.map((target) => ({ ...target }))
16691
+ };
16692
+ }
16693
+ cloneGroupTargetSet(value) {
16694
+ return {
16695
+ targets: value.targets.map((target) => ({ ...target }))
16696
+ };
16697
+ }
16553
16698
  async onConnected(opts) {
16554
16699
  const client = this.client;
16555
16700
  try {
16556
16701
  await client._initV2Session();
16702
+ this.clearV2SessionInitRetry();
16557
16703
  } catch (exc) {
16558
16704
  client._clientLog.warn(`V2 session init failed (non-fatal): ${formatE2EEError(exc)}`);
16705
+ if (isTransientV2SessionInitError(exc)) {
16706
+ this.scheduleV2SessionInitRetry(1, opts.backgroundSync, exc);
16707
+ }
16559
16708
  }
16560
16709
  if (opts.backgroundSync && client._v2Session && typeof client._v2AutoConfirmPendingProposals === "function") {
16561
16710
  client._safeAsync(client._v2AutoConfirmPendingProposals());
16562
16711
  }
16563
16712
  }
16713
+ currentV2SessionInitRetryIdentityKey() {
16714
+ const client = this.client;
16715
+ if (!client._aid) return null;
16716
+ return JSON.stringify([
16717
+ client._aid,
16718
+ client._deviceId ?? "",
16719
+ client._currentAid ?? null,
16720
+ Number(client._v2RuntimeGeneration ?? 0)
16721
+ ]);
16722
+ }
16723
+ canRetryV2SessionInit(identityKey) {
16724
+ const client = this.client;
16725
+ return client.state === "ready" /* READY */ && !client._closing && this.currentV2SessionInitRetryIdentityKey() === identityKey;
16726
+ }
16727
+ clearV2SessionInitRetry() {
16728
+ if (this.v2SessionInitRetryTimer) {
16729
+ globalThis.clearTimeout(this.v2SessionInitRetryTimer);
16730
+ this.v2SessionInitRetryTimer = null;
16731
+ }
16732
+ this.v2SessionInitRetryBackgroundSync = false;
16733
+ this.v2SessionInitRetryIdentityKey = null;
16734
+ }
16735
+ scheduleV2SessionInitRetry(attempt, backgroundSync, reason) {
16736
+ const client = this.client;
16737
+ if (attempt > V2_SESSION_INIT_RETRY_LIMIT) {
16738
+ this.v2SessionInitRetryBackgroundSync = false;
16739
+ this.v2SessionInitRetryIdentityKey = null;
16740
+ client._clientLog.warn(`V2 session init retry exhausted: attempts=${V2_SESSION_INIT_RETRY_LIMIT} last_error=${formatE2EEError(reason)}`);
16741
+ return;
16742
+ }
16743
+ const identityKey = this.currentV2SessionInitRetryIdentityKey();
16744
+ if (!identityKey || !this.canRetryV2SessionInit(identityKey)) return;
16745
+ if (this.v2SessionInitRetryIdentityKey && this.v2SessionInitRetryIdentityKey !== identityKey) {
16746
+ this.clearV2SessionInitRetry();
16747
+ }
16748
+ if (this.v2SessionInitRetryTimer) {
16749
+ this.v2SessionInitRetryBackgroundSync = this.v2SessionInitRetryBackgroundSync || backgroundSync;
16750
+ return;
16751
+ }
16752
+ this.v2SessionInitRetryIdentityKey = identityKey;
16753
+ this.v2SessionInitRetryBackgroundSync = this.v2SessionInitRetryBackgroundSync || backgroundSync;
16754
+ const delayMs = Math.min(
16755
+ V2_SESSION_INIT_RETRY_BASE_MS * attempt,
16756
+ V2_SESSION_INIT_RETRY_MAX_MS
16757
+ );
16758
+ client._clientLog.debug(`V2 session init retry scheduled: attempt=${attempt} delay_ms=${delayMs} reason=${formatE2EEError(reason)}`);
16759
+ this.v2SessionInitRetryTimer = globalThis.setTimeout(() => {
16760
+ this.v2SessionInitRetryTimer = null;
16761
+ if (!this.canRetryV2SessionInit(identityKey)) {
16762
+ this.clearV2SessionInitRetry();
16763
+ return;
16764
+ }
16765
+ client._safeAsync(this.retryV2SessionInit(attempt, identityKey));
16766
+ }, delayMs);
16767
+ }
16768
+ async retryV2SessionInit(attempt, identityKey) {
16769
+ const client = this.client;
16770
+ if (!this.canRetryV2SessionInit(identityKey)) {
16771
+ this.clearV2SessionInitRetry();
16772
+ return;
16773
+ }
16774
+ try {
16775
+ await client._initV2Session();
16776
+ const backgroundSync = this.v2SessionInitRetryBackgroundSync;
16777
+ this.clearV2SessionInitRetry();
16778
+ client._clientLog.debug(`V2 session init retry succeeded: attempt=${attempt}`);
16779
+ if (backgroundSync && client._v2Session) {
16780
+ if (typeof client._v2AutoConfirmPendingProposals === "function") {
16781
+ client._safeAsync(client._v2AutoConfirmPendingProposals());
16782
+ }
16783
+ if (typeof client._fillP2pGap === "function") {
16784
+ client._safeAsync(client._fillP2pGap());
16785
+ }
16786
+ }
16787
+ } catch (exc) {
16788
+ client._clientLog.warn(`V2 session init retry failed: attempt=${attempt} err=${formatE2EEError(exc)}`);
16789
+ if (isTransientV2SessionInitError(exc)) {
16790
+ this.scheduleV2SessionInitRetry(attempt + 1, this.v2SessionInitRetryBackgroundSync, exc);
16791
+ } else {
16792
+ this.clearV2SessionInitRetry();
16793
+ }
16794
+ }
16795
+ }
16564
16796
  async initV2Session() {
16565
16797
  const client = this.client;
16566
16798
  if (!client._aid) return;
@@ -16859,15 +17091,28 @@ var V2E2EECoordinator = class {
16859
17091
  const decrypted = [];
16860
17092
  let nextAfterSeq = opts?.force ? afterSeq : afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0);
16861
17093
  let pageCount = 0;
17094
+ let lastAutoAckSeq = 0;
17095
+ let pendingAckSeq = 0;
16862
17096
  const maxPages = 100;
16863
17097
  while (pageCount < maxPages) {
16864
17098
  pageCount += 1;
17099
+ const ackUpToSeq = pendingAckSeq;
16865
17100
  const result = await client._callRawV2Rpc("message.v2.pull", {
16866
17101
  after_seq: nextAfterSeq,
16867
17102
  limit,
16868
- ...opts?.force ? { force: true } : {}
17103
+ ...opts?.force ? { force: true } : {},
17104
+ ...ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}
17105
+ });
17106
+ if (ackUpToSeq > 0) {
17107
+ pendingAckSeq = 0;
17108
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
17109
+ }
17110
+ const pageMessages = Array.isArray(result?.messages) ? result.messages : [];
17111
+ const messages = pageMessages.filter((msg) => {
17112
+ const seq = Number(msg.seq ?? 0);
17113
+ return Number.isFinite(seq) && seq > nextAfterSeq;
16869
17114
  });
16870
- const messages = Array.isArray(result?.messages) ? result.messages : [];
17115
+ client._clientLog.debug(`message.v2.pull page response: page=${pageCount}, raw_count=${messages.length}, stale_count=${Math.max(0, pageMessages.length - messages.length)}, has_more=${String(result.has_more ?? "")}, server_ack_seq=${String(result.server_ack_seq ?? "")}`);
16871
17116
  const seqs = messages.map((msg) => Number(msg.seq ?? 0)).filter((seq) => Number.isFinite(seq) && seq > 0);
16872
17117
  const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
16873
17118
  const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
@@ -16935,19 +17180,37 @@ var V2E2EECoordinator = class {
16935
17180
  const contigAdvanced = ackSeq !== pageContigBefore;
16936
17181
  if (contigAdvanced) {
16937
17182
  await client._drainOrderedMessages(ns);
16938
- client._saveSeqTrackerState();
17183
+ client._persistSeq(ns);
16939
17184
  }
16940
- const ackNeeded = messages.length > 0 && ackSeq > 0 && (contigAdvanced || hasServerAckSeq && ackSeq > serverAckSeq);
17185
+ const ackNeeded = pageMessages.length > 0 && ackSeq > 0 && ackSeq > lastAutoAckSeq && (contigAdvanced || hasServerAckSeq && ackSeq > serverAckSeq);
16941
17186
  if (ackNeeded) {
16942
- await this.ackV2(ackSeq);
17187
+ const nextAfter2 = Math.max(pageMaxSeq, nextAfterSeq);
17188
+ const canContinuePage2 = messages.length > 0 && nextAfter2 > nextAfterSeq;
17189
+ if (canContinuePage2) {
17190
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
17191
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
17192
+ } else {
17193
+ await this.ackV2(ackSeq);
17194
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
17195
+ }
16943
17196
  }
16944
17197
  }
16945
17198
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
16946
- if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false) break;
17199
+ const rawCount = messages.length;
17200
+ const canContinuePage = rawCount > 0 && nextAfter > nextAfterSeq;
17201
+ const fullPage = canContinuePage && limit > 0 && rawCount >= limit;
17202
+ const shouldContinue = canContinuePage && (fullPage || pendingAckSeq > 0);
17203
+ if (!shouldContinue) break;
17204
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
17205
+ if (tailDelayMs > 0) await v2Sleep(tailDelayMs);
16947
17206
  nextAfterSeq = nextAfter;
16948
17207
  }
16949
17208
  if (pageCount >= maxPages) {
16950
17209
  client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
17210
+ if (pendingAckSeq > 0) {
17211
+ await this.ackV2(pendingAckSeq);
17212
+ pendingAckSeq = 0;
17213
+ }
16951
17214
  }
16952
17215
  return decrypted;
16953
17216
  }
@@ -17011,7 +17274,7 @@ var V2E2EECoordinator = class {
17011
17274
  const ns = `group:${gid}`;
17012
17275
  client._seqTracker.onMessageSeq(ns, seq);
17013
17276
  client._markPublishedSeq(ns, seq);
17014
- client._saveSeqTrackerState();
17277
+ await client._saveSeqTrackerState();
17015
17278
  }
17016
17279
  }
17017
17280
  return client._delivery.attachSendResultEnvelope("group.send", {
@@ -17034,7 +17297,7 @@ var V2E2EECoordinator = class {
17034
17297
  const ns = `group:${gid}`;
17035
17298
  client._seqTracker.onMessageSeq(ns, seq);
17036
17299
  client._markPublishedSeq(ns, seq);
17037
- client._saveSeqTrackerState();
17300
+ await client._saveSeqTrackerState();
17038
17301
  }
17039
17302
  }
17040
17303
  return client._delivery.attachSendResultEnvelope("group.send", {
@@ -17059,20 +17322,34 @@ var V2E2EECoordinator = class {
17059
17322
  if (!gid) throw new ValidationError("group.pull requires group_id");
17060
17323
  const ns = `group:${gid}`;
17061
17324
  const decrypted = [];
17325
+ const wireGroupId = String(opts?.wireGroupId ?? groupId ?? "").trim() || gid;
17062
17326
  const cursorParams = opts?.cursorParams ?? {};
17063
17327
  const ownsCursor = opts?.ownsCursor !== false;
17064
17328
  let nextAfterSeq = opts?.explicitAfterSeq ? afterSeq : afterSeq || client._seqTracker.getContiguousSeq(ns);
17065
17329
  let pageCount = 0;
17330
+ let lastAutoAckSeq = 0;
17331
+ let pendingAckSeq = 0;
17066
17332
  const maxPages = 100;
17067
17333
  while (pageCount < maxPages) {
17068
17334
  pageCount += 1;
17335
+ const ackUpToSeq = ownsCursor ? pendingAckSeq : 0;
17069
17336
  const result = await client._callRawV2Rpc("group.v2.pull", {
17070
- group_id: gid,
17337
+ group_id: wireGroupId,
17338
+ group_aid: gid,
17071
17339
  after_seq: nextAfterSeq,
17072
17340
  limit,
17073
- ...cursorParams
17341
+ ...cursorParams,
17342
+ ...ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}
17343
+ });
17344
+ if (ackUpToSeq > 0) {
17345
+ pendingAckSeq = 0;
17346
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
17347
+ }
17348
+ const pageMessages = Array.isArray(result?.messages) ? result.messages : [];
17349
+ const messages = pageMessages.filter((msg) => {
17350
+ const seq = Number(msg.seq ?? 0);
17351
+ return Number.isFinite(seq) && seq > nextAfterSeq;
17074
17352
  });
17075
- const messages = Array.isArray(result?.messages) ? result.messages : [];
17076
17353
  const seqs = messages.map((msg) => Number(msg.seq ?? 0)).filter((seq) => Number.isFinite(seq) && seq > 0);
17077
17354
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
17078
17355
  const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
@@ -17152,20 +17429,38 @@ var V2E2EECoordinator = class {
17152
17429
  const contigAdvanced = ackSeq !== pageContigBefore;
17153
17430
  if (contigAdvanced) {
17154
17431
  await client._drainOrderedMessages(ns);
17155
- client._saveSeqTrackerState();
17432
+ client._persistSeq(ns);
17156
17433
  }
17157
17434
  const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, "current_seq");
17158
- const ackNeeded = messages.length > 0 && ackSeq > 0 && ownsCursor && (contigAdvanced || hasServerCursor && ackSeq > cursorCurrentSeq);
17435
+ const ackNeeded = pageMessages.length > 0 && ackSeq > 0 && ackSeq > lastAutoAckSeq && ownsCursor && (contigAdvanced || hasServerCursor && ackSeq > cursorCurrentSeq);
17159
17436
  if (ackNeeded) {
17160
- await this.ackGroupV2(gid, ackSeq);
17437
+ const nextAfter2 = Math.max(pageMaxSeq, nextAfterSeq);
17438
+ const canContinuePage2 = messages.length > 0 && nextAfter2 > nextAfterSeq && ownsCursor;
17439
+ if (canContinuePage2) {
17440
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
17441
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
17442
+ } else {
17443
+ await this.ackGroupV2(gid, ackSeq);
17444
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
17445
+ }
17161
17446
  }
17162
17447
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
17163
17448
  if (!ownsCursor) break;
17164
- if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false) break;
17449
+ const rawCount = messages.length;
17450
+ const canContinuePage = rawCount > 0 && nextAfter > nextAfterSeq;
17451
+ const fullPage = canContinuePage && limit > 0 && rawCount >= limit;
17452
+ const shouldContinue = canContinuePage && (fullPage || pendingAckSeq > 0);
17453
+ if (!shouldContinue) break;
17454
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
17455
+ if (tailDelayMs > 0) await v2Sleep(tailDelayMs);
17165
17456
  nextAfterSeq = nextAfter;
17166
17457
  }
17167
17458
  if (pageCount >= maxPages) {
17168
17459
  client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
17460
+ if (pendingAckSeq > 0) {
17461
+ await this.ackGroupV2(gid, pendingAckSeq);
17462
+ pendingAckSeq = 0;
17463
+ }
17169
17464
  }
17170
17465
  return decrypted;
17171
17466
  }
@@ -17177,7 +17472,7 @@ var V2E2EECoordinator = class {
17177
17472
  let seq = Number(upToSeq ?? client._seqTracker.getContiguousSeq(ns));
17178
17473
  if (!Number.isFinite(seq) || seq <= 0) return { acked: 0 };
17179
17474
  seq = client._clampAckSeq("group.v2.ack", "up_to_seq", ns, seq);
17180
- return client._callRawV2Rpc("group.v2.ack", { group_id: gid, up_to_seq: seq });
17475
+ return client._callRawV2Rpc("group.v2.ack", { group_id: gid, group_aid: gid, up_to_seq: seq });
17181
17476
  }
17182
17477
  async putMessageThoughtEncryptedV2(params2) {
17183
17478
  const client = this.client;
@@ -17646,6 +17941,7 @@ var V2E2EECoordinator = class {
17646
17941
  auditRaw = cached.auditRecipients ?? [];
17647
17942
  wrapPolicy = cached.wrapPolicy;
17648
17943
  } else {
17944
+ this.targetSetCache.delete(to);
17649
17945
  const bs = await client.call("message.v2.bootstrap", {
17650
17946
  peer_aid: to,
17651
17947
  e2ee_wrap_capabilities: v2WrapCapabilities()
@@ -17665,69 +17961,80 @@ var V2E2EECoordinator = class {
17665
17961
  if (peerDevices.length === 0) {
17666
17962
  throw new E2EEError(`V2 bootstrap: no devices found for ${to}`);
17667
17963
  }
17668
- const targets = [];
17669
- for (const dev of peerDevices) {
17670
- const devId = getV2DeviceId(dev);
17671
- const target = await client._v2BuildTargetFromDevice({
17672
- dev,
17673
- aid: to,
17674
- deviceId: devId.value,
17675
- role: "peer",
17676
- defaultKeySource: "peer_device_prekey"
17677
- });
17678
- if (target) targets.push(target);
17679
- }
17680
- for (const dev of auditRaw) {
17681
- const target = await client._v2BuildTargetFromDevice({
17682
- dev,
17683
- aid: String(dev.aid ?? ""),
17684
- deviceId: String(dev.device_id ?? ""),
17685
- role: "audit",
17686
- defaultKeySource: "peer_device_prekey"
17687
- });
17688
- if (target) targets.push(target);
17689
- }
17690
- if (client._aid && client._aid !== to) {
17691
- try {
17692
- const selfCached = this.getBootstrapCacheEntry(client._aid);
17693
- let selfDevices = [];
17694
- if (selfCached && Date.now() - Number(selfCached.cachedAt ?? 0) < V2_BOOTSTRAP_TTL_MS) {
17695
- selfDevices = selfCached.devices ?? [];
17696
- } else {
17697
- const selfBs = await client.call("message.v2.bootstrap", {
17698
- peer_aid: client._aid,
17699
- e2ee_wrap_capabilities: v2WrapCapabilities()
17700
- });
17701
- selfDevices = Array.isArray(selfBs?.peer_devices) ? selfBs.peer_devices : [];
17702
- if (selfDevices.length > 0) {
17703
- this.setBootstrapCacheEntry(client._aid, {
17704
- devices: selfDevices,
17705
- auditRecipients: [],
17706
- cachedAt: Date.now()
17964
+ let targetSet = useCache ? this.getTargetSetCache(to) : void 0;
17965
+ if (targetSet) {
17966
+ targetSet = this.cloneP2PTargetSet(targetSet);
17967
+ } else {
17968
+ const targets = [];
17969
+ for (const dev of peerDevices) {
17970
+ const devId = getV2DeviceId(dev);
17971
+ const target = await client._v2BuildTargetFromDevice({
17972
+ dev,
17973
+ aid: to,
17974
+ deviceId: devId.value,
17975
+ role: "peer",
17976
+ defaultKeySource: "peer_device_prekey"
17977
+ });
17978
+ if (target) targets.push(target);
17979
+ }
17980
+ const auditTargets = [];
17981
+ for (const dev of auditRaw) {
17982
+ const target = await client._v2BuildTargetFromDevice({
17983
+ dev,
17984
+ aid: String(dev.aid ?? ""),
17985
+ deviceId: String(dev.device_id ?? ""),
17986
+ role: "audit",
17987
+ defaultKeySource: "peer_device_prekey"
17988
+ });
17989
+ if (target) auditTargets.push(target);
17990
+ }
17991
+ if (client._aid && client._aid !== to) {
17992
+ try {
17993
+ const selfCached = this.getBootstrapCacheEntry(client._aid);
17994
+ let selfDevices = [];
17995
+ if (selfCached && Date.now() - Number(selfCached.cachedAt ?? 0) < V2_BOOTSTRAP_TTL_MS) {
17996
+ selfDevices = selfCached.devices ?? [];
17997
+ } else {
17998
+ this.targetSetCache.delete(client._aid);
17999
+ const selfBs = await client.call("message.v2.bootstrap", {
18000
+ peer_aid: client._aid,
18001
+ e2ee_wrap_capabilities: v2WrapCapabilities()
17707
18002
  });
18003
+ selfDevices = Array.isArray(selfBs?.peer_devices) ? selfBs.peer_devices : [];
18004
+ if (selfDevices.length > 0) {
18005
+ this.setBootstrapCacheEntry(client._aid, {
18006
+ devices: selfDevices,
18007
+ auditRecipients: [],
18008
+ cachedAt: Date.now()
18009
+ });
18010
+ }
17708
18011
  }
18012
+ for (const dev of selfDevices) {
18013
+ const devId = getV2DeviceId(dev);
18014
+ if (!devId.present || devId.value === client._deviceId) continue;
18015
+ const target = await client._v2BuildTargetFromDevice({
18016
+ dev,
18017
+ aid: client._aid,
18018
+ deviceId: devId.value,
18019
+ role: "self_sync",
18020
+ defaultKeySource: "peer_device_prekey"
18021
+ });
18022
+ if (target) targets.push(target);
18023
+ }
18024
+ } catch (exc) {
18025
+ client._clientLog.debug(`V2 thought self-sync bootstrap failed (non-fatal): ${String(exc)}`);
17709
18026
  }
17710
- for (const dev of selfDevices) {
17711
- const devId = getV2DeviceId(dev);
17712
- if (!devId.present || devId.value === client._deviceId) continue;
17713
- const target = await client._v2BuildTargetFromDevice({
17714
- dev,
17715
- aid: client._aid,
17716
- deviceId: devId.value,
17717
- role: "self_sync",
17718
- defaultKeySource: "peer_device_prekey"
17719
- });
17720
- if (target) targets.push(target);
17721
- }
17722
- } catch (exc) {
17723
- client._clientLog.debug(`V2 thought self-sync bootstrap failed (non-fatal): ${String(exc)}`);
17724
18027
  }
18028
+ targetSet = this.setTargetSetCache(to, {
18029
+ targets: applyV2WrapPolicyToTargets(targets, wrapPolicy),
18030
+ auditRecipients: applyV2WrapPolicyToTargets(auditTargets, wrapPolicy)
18031
+ });
18032
+ targetSet = this.cloneP2PTargetSet(targetSet);
17725
18033
  }
17726
18034
  const sender = await session.getSenderIdentity();
17727
- const sendTargets = applyV2WrapPolicyToTargets(targets, wrapPolicy);
17728
18035
  const envelope = await encryptP2PMessage(
17729
18036
  sender,
17730
- { targets: sendTargets, auditRecipients: [] },
18037
+ targetSet,
17731
18038
  opts.payload,
17732
18039
  { messageId: opts.messageId, timestamp: opts.timestamp, protectedHeaders: opts.protectedHeaders, context: opts.context }
17733
18040
  );
@@ -17739,7 +18046,8 @@ var V2E2EECoordinator = class {
17739
18046
  throw new StateError("V2 session not initialized");
17740
18047
  }
17741
18048
  const session = client._v2Session;
17742
- const groupId = normalizeGroupId(opts.groupId) || String(opts.groupId ?? "").trim();
18049
+ const rawGroupId = String(opts.groupId ?? "").trim();
18050
+ const groupId = normalizeGroupId(rawGroupId) || rawGroupId;
17743
18051
  if (!groupId) throw new ValidationError("group.send requires 'group_id'");
17744
18052
  const useCache = opts.useCache !== false;
17745
18053
  const cacheKey = `group:${groupId}`;
@@ -17748,14 +18056,25 @@ var V2E2EECoordinator = class {
17748
18056
  let stateCommitment = { state_version: 0, state_hash: "", state_chain: "" };
17749
18057
  let auditRecipientsRaw = [];
17750
18058
  let wrapPolicy;
17751
- const cached = useCache ? this.getBootstrapCacheEntry(cacheKey) : void 0;
18059
+ let usedCachedBootstrap = false;
18060
+ let cached = useCache ? this.getBootstrapCacheEntry(cacheKey) : void 0;
18061
+ if (!cached && useCache && rawGroupId && rawGroupId !== groupId) {
18062
+ const legacyCacheKey = `group:${rawGroupId}`;
18063
+ cached = this.getBootstrapCacheEntry(legacyCacheKey);
18064
+ if (cached) {
18065
+ this.setBootstrapCacheEntry(cacheKey, cached);
18066
+ this.deleteBootstrapCacheEntry(legacyCacheKey);
18067
+ }
18068
+ }
17752
18069
  if (cached && Date.now() - Number(cached.cachedAt ?? 0) < V2_BOOTSTRAP_TTL_MS) {
17753
18070
  allDevices = cached.devices ?? [];
17754
18071
  epoch = Number(cached.epoch ?? 0) || 0;
17755
18072
  stateCommitment = cached.stateCommitment ?? { state_version: 0, state_hash: "", state_chain: "" };
17756
18073
  auditRecipientsRaw = cached.auditRecipients ?? [];
17757
18074
  wrapPolicy = cached.wrapPolicy;
18075
+ usedCachedBootstrap = true;
17758
18076
  } else {
18077
+ this.targetSetCache.delete(cacheKey);
17759
18078
  const bs = await client.call("group.v2.bootstrap", {
17760
18079
  group_id: groupId,
17761
18080
  e2ee_wrap_capabilities: v2WrapCapabilities()
@@ -17790,41 +18109,64 @@ var V2E2EECoordinator = class {
17790
18109
  if (allDevices.length === 0) {
17791
18110
  throw new E2EEError(`V2 group bootstrap: no devices found for group ${groupId}`);
17792
18111
  }
17793
- const targets = [];
17794
- for (const dev of allDevices) {
17795
- const devAid = String(dev.aid ?? "").trim();
17796
- const devId = getV2DeviceId(dev);
17797
- if (devAid === client._aid && devId.present && devId.value === client._deviceId) continue;
17798
- const role = devAid === client._aid ? "self_sync" : "member";
17799
- const target = await client._v2BuildTargetFromDevice({
17800
- dev,
17801
- aid: devAid,
17802
- deviceId: devId.value,
17803
- role,
17804
- defaultKeySource: "peer_device_prekey"
18112
+ let targetSet = useCache ? this.getTargetSetCache(cacheKey) : void 0;
18113
+ if (targetSet) {
18114
+ targetSet = this.cloneGroupTargetSet(targetSet);
18115
+ } else {
18116
+ const targets = [];
18117
+ for (const dev of allDevices) {
18118
+ const devAid = String(dev.aid ?? "").trim();
18119
+ const devId = getV2DeviceId(dev);
18120
+ if (devAid === client._aid && devId.present && devId.value === client._deviceId) continue;
18121
+ const role = devAid === client._aid ? "self_sync" : "member";
18122
+ const target = await client._v2BuildTargetFromDevice({
18123
+ dev,
18124
+ aid: devAid,
18125
+ deviceId: devId.value,
18126
+ role,
18127
+ defaultKeySource: "peer_device_prekey"
18128
+ });
18129
+ if (target) targets.push(target);
18130
+ }
18131
+ if (targets.length === 0) {
18132
+ if (usedCachedBootstrap) {
18133
+ throw new E2EEError(`V2 group: no target devices for group ${groupId}; bootstrap may be stale`, {
18134
+ code: V2_GROUP_STALE_BOOTSTRAP_CODE,
18135
+ data: { expected_peer_aids: [] }
18136
+ });
18137
+ }
18138
+ throw new E2EEError(`V2 group: no target devices for group ${groupId}`);
18139
+ }
18140
+ for (const dev of auditRecipientsRaw) {
18141
+ const target = await client._v2BuildTargetFromDevice({
18142
+ dev,
18143
+ aid: String(dev.aid ?? ""),
18144
+ deviceId: String(dev.device_id ?? ""),
18145
+ role: "audit",
18146
+ defaultKeySource: "peer_device_prekey"
18147
+ });
18148
+ if (target) targets.push(target);
18149
+ }
18150
+ targetSet = this.setTargetSetCache(cacheKey, {
18151
+ targets: applyV2WrapPolicyToTargets(targets, wrapPolicy)
17805
18152
  });
17806
- if (target) targets.push(target);
18153
+ targetSet = this.cloneGroupTargetSet(targetSet);
17807
18154
  }
17808
- if (targets.length === 0) {
18155
+ if (targetSet.targets.length === 0) {
18156
+ if (usedCachedBootstrap) {
18157
+ throw new E2EEError(`V2 group: no target devices for group ${groupId}; bootstrap may be stale`, {
18158
+ code: V2_GROUP_STALE_BOOTSTRAP_CODE,
18159
+ data: { expected_peer_aids: [] }
18160
+ });
18161
+ }
17809
18162
  throw new E2EEError(`V2 group: no target devices for group ${groupId}`);
17810
18163
  }
17811
- for (const dev of auditRecipientsRaw) {
17812
- const target = await client._v2BuildTargetFromDevice({
17813
- dev,
17814
- aid: String(dev.aid ?? ""),
17815
- deviceId: String(dev.device_id ?? ""),
17816
- role: "audit",
17817
- defaultKeySource: "peer_device_prekey"
17818
- });
17819
- if (target) targets.push(target);
17820
- }
17821
18164
  const sender = await session.getSenderIdentity();
17822
- const sendTargets = applyV2WrapPolicyToTargets(targets, wrapPolicy);
17823
18165
  const envelope = await encryptGroupMessage(
17824
18166
  sender,
17825
18167
  groupId,
17826
18168
  epoch,
17827
- sendTargets,
18169
+ targetSet.targets,
17828
18170
  opts.payload,
17829
18171
  { messageId: opts.messageId, timestamp: opts.timestamp, protectedHeaders: opts.protectedHeaders, context: opts.context },
17830
18172
  stateCommitment
@@ -17843,23 +18185,6 @@ var V2E2EECoordinator = class {
17843
18185
  this.scheduleGroupSpkRotation(groupId, { reason: `group_changed:${action}` });
17844
18186
  }
17845
18187
  }
17846
- async handleV2EpochRotated(data) {
17847
- const client = this.client;
17848
- if (!data || typeof data !== "object" || Array.isArray(data)) return;
17849
- const d = data;
17850
- const groupId = String(d.group_id ?? "").trim();
17851
- if (!groupId) return;
17852
- const epoch = d.epoch ?? 0;
17853
- client._clientLog.debug(`_onV2EpochRotated: group=${groupId} epoch=${String(epoch)}`);
17854
- this.deleteBootstrapCacheEntry(`group:${groupId}`);
17855
- if (!client._v2Session) return;
17856
- try {
17857
- await client._v2Session.rotateSPK(client._v2CallFn());
17858
- client._clientLog.info(`SPK rotated after epoch change: group=${groupId} epoch=${String(epoch)}`);
17859
- } catch (exc) {
17860
- client._clientLog.debug(`SPK rotation after epoch change failed (non-fatal): ${formatE2EEError(exc)}`);
17861
- }
17862
- }
17863
18188
  };
17864
18189
 
17865
18190
  // src/client/group-state.ts
@@ -17901,7 +18226,8 @@ async function computeStateCommitment(groupId, epoch, statePayload) {
17901
18226
  }
17902
18227
  const sorted = deepClone2(statePayload);
17903
18228
  sortPayload(sorted);
17904
- const groupBytes = new TextEncoder().encode(groupId);
18229
+ const groupKey = normalizeGroupId(groupId) || String(groupId ?? "").trim();
18230
+ const groupBytes = new TextEncoder().encode(groupKey);
17905
18231
  const epochBytes = new Uint8Array(4);
17906
18232
  new DataView(epochBytes.buffer).setUint32(0, epoch, false);
17907
18233
  const payloadBytes = canonicalJson(sorted);
@@ -18057,6 +18383,13 @@ function normalizedGroupId(raw) {
18057
18383
  const groupId = String(raw ?? "").trim();
18058
18384
  return normalizeGroupId(groupId) || groupId;
18059
18385
  }
18386
+ function groupCacheIds(raw, groupId) {
18387
+ const ids = [];
18388
+ for (const value of [groupId, String(raw ?? "").trim()]) {
18389
+ if (value && !ids.includes(value)) ids.push(value);
18390
+ }
18391
+ return ids;
18392
+ }
18060
18393
  var GroupStateCoordinator = class {
18061
18394
  constructor(runtime) {
18062
18395
  __publicField(this, "runtime");
@@ -18086,12 +18419,15 @@ var GroupStateCoordinator = class {
18086
18419
  }
18087
18420
  handleGroupChangedV2Membership(data) {
18088
18421
  const client = this.client;
18089
- const groupId = normalizedGroupId(data.group_id);
18422
+ const rawGroupId = data.group_aid ?? data.group_id;
18423
+ const groupId = normalizedGroupId(rawGroupId);
18090
18424
  const action = String(data.action ?? "").trim();
18091
18425
  if (!groupId) {
18092
18426
  return;
18093
18427
  }
18094
- client._v2E2EE?.deleteBootstrapCacheEntry?.(`group:${groupId}`);
18428
+ for (const cacheGroupId of groupCacheIds(rawGroupId, groupId)) {
18429
+ client._v2E2EE?.deleteBootstrapCacheEntry?.(`group:${cacheGroupId}`);
18430
+ }
18095
18431
  const membershipAction = MEMBERSHIP_ACTIONS2.has(action);
18096
18432
  if (client._v2Session && (action === "upsert" || membershipAction)) {
18097
18433
  client._safeAsync(client._v2AutoProposeState(groupId, { leaderDelay: true }));
@@ -18101,7 +18437,7 @@ var GroupStateCoordinator = class {
18101
18437
  async onV2StateProposed(data) {
18102
18438
  const client = this.client;
18103
18439
  if (!isJsonObject(data) || !client._v2Session) return;
18104
- const groupId = normalizedGroupId(data.group_id);
18440
+ const groupId = normalizedGroupId(data.group_aid ?? data.group_id);
18105
18441
  if (!groupId) return;
18106
18442
  await client._dispatcher.publish("group.v2.state_proposed", data);
18107
18443
  try {
@@ -18113,7 +18449,7 @@ var GroupStateCoordinator = class {
18113
18449
  async onV2StateRetryNeeded(data) {
18114
18450
  const client = this.client;
18115
18451
  if (!isJsonObject(data) || !client._v2Session) return;
18116
- const groupId = normalizedGroupId(data.group_id);
18452
+ const groupId = normalizedGroupId(data.group_aid ?? data.group_id);
18117
18453
  if (!groupId) return;
18118
18454
  await client._dispatcher.publish("group.v2.state_retry_needed", data);
18119
18455
  try {
@@ -18125,9 +18461,12 @@ var GroupStateCoordinator = class {
18125
18461
  async onV2StateConfirmed(data) {
18126
18462
  const client = this.client;
18127
18463
  if (!isJsonObject(data)) return;
18128
- const groupId = normalizedGroupId(data.group_id);
18464
+ const rawGroupId = data.group_aid ?? data.group_id;
18465
+ const groupId = normalizedGroupId(rawGroupId);
18129
18466
  if (groupId) {
18130
- client._v2E2EE?.deleteBootstrapCacheEntry?.(`group:${groupId}`);
18467
+ for (const cacheGroupId of groupCacheIds(rawGroupId, groupId)) {
18468
+ client._v2E2EE?.deleteBootstrapCacheEntry?.(`group:${cacheGroupId}`);
18469
+ }
18131
18470
  client._v2AutoProposeLastSnapshot?.delete?.(groupId);
18132
18471
  }
18133
18472
  await client._dispatcher.publish("group.v2.state_confirmed", data);
@@ -18259,14 +18598,16 @@ var GroupStateCoordinator = class {
18259
18598
  async onGroupStateCommitted(data) {
18260
18599
  const client = this.client;
18261
18600
  const tStart = Date.now();
18262
- const groupIdInit = String(data?.group_id ?? "");
18601
+ const groupIdInit = String(
18602
+ data?.group_aid ?? data?.group_id ?? ""
18603
+ );
18263
18604
  client._clientLog.debug(`_onGroupStateCommitted enter: group_id=${groupIdInit} state_version=${String(data?.state_version ?? "-")}`);
18264
18605
  try {
18265
18606
  if (!isJsonObject(data)) {
18266
18607
  client._clientLog.debug(`_onGroupStateCommitted exit: elapsed=${Date.now() - tStart}ms reason=non_object`);
18267
18608
  return;
18268
18609
  }
18269
- const groupId = String(data.group_id ?? "").trim();
18610
+ const groupId = normalizedGroupId(data.group_aid ?? data.group_id);
18270
18611
  if (!groupId) {
18271
18612
  client._clientLog.debug(`_onGroupStateCommitted exit: elapsed=${Date.now() - tStart}ms reason=no_group_id`);
18272
18613
  return;
@@ -18700,7 +19041,7 @@ var GroupStateCoordinator = class {
18700
19041
  const groupsResp = await client.call("group.list_my", {});
18701
19042
  const groups = Array.isArray(groupsResp?.groups) ? groupsResp.groups : Array.isArray(groupsResp?.items) ? groupsResp.items : [];
18702
19043
  for (const g of groups) {
18703
- const gid = String(g.group_id ?? "").trim();
19044
+ const gid = normalizedGroupId(g.group_aid ?? g.group_id);
18704
19045
  const myRole = String(g.role ?? g.my_role ?? "").trim();
18705
19046
  if (!gid || myRole !== "owner" && myRole !== "admin") continue;
18706
19047
  try {
@@ -18719,7 +19060,7 @@ var GroupStateCoordinator = class {
18719
19060
  extractGroupIdFromMutationResult(result, params2) {
18720
19061
  const client = this.client;
18721
19062
  const extracted = typeof client._extractGroupIdFromResult === "function" ? client._extractGroupIdFromResult(result) : "";
18722
- return normalizedGroupId(extracted || params2.group_id || "");
19063
+ return normalizedGroupId(extracted || params2.group_aid || params2.group_id || "");
18723
19064
  }
18724
19065
  pruneSigCache(now) {
18725
19066
  const client = this.client;
@@ -18753,8 +19094,16 @@ var GroupStateCoordinator = class {
18753
19094
  if (extra.length === 0) return;
18754
19095
  let mode = "";
18755
19096
  try {
18756
- const reqResp = await client.call("group.get_join_requirements", { group_id: groupId });
18757
- mode = isJsonObject(reqResp) ? String(reqResp.mode ?? "").trim() : "";
19097
+ const reqResp = await client.call("group.get_settings", { group_id: groupId, keys: ["join.mode"] });
19098
+ const settings = {};
19099
+ if (isJsonObject(reqResp) && Array.isArray(reqResp.settings)) {
19100
+ for (const s of reqResp.settings) {
19101
+ if (isJsonObject(s) && typeof s.key === "string") {
19102
+ settings[s.key] = s.value;
19103
+ }
19104
+ }
19105
+ }
19106
+ mode = String(settings["join.mode"] ?? "").trim();
18758
19107
  } catch {
18759
19108
  mode = "";
18760
19109
  }
@@ -18813,9 +19162,13 @@ var AUNLogger = class {
18813
19162
  error: (msg, ...args) => this._emit("ERROR", module, msg, args),
18814
19163
  warn: (msg, ...args) => this._emit("WARN", module, msg, args),
18815
19164
  info: (msg, ...args) => this._emit("INFO", module, msg, args),
18816
- debug: (msg, ...args) => this._emit("DEBUG", module, msg, args)
19165
+ debug: (msg, ...args) => this._emit("DEBUG", module, msg, args),
19166
+ isDebugEnabled: () => this.isDebugEnabled()
18817
19167
  };
18818
19168
  }
19169
+ isDebugEnabled() {
19170
+ return this._debug && this._minLevel <= LEVEL_ORDER.DEBUG;
19171
+ }
18819
19172
  bindAid(aid) {
18820
19173
  this._aid = aid || null;
18821
19174
  }
@@ -19439,9 +19792,6 @@ var _AUNClient = class _AUNClient {
19439
19792
  this._dispatcher.subscribe("_raw.group.v2.message_created", (data) => {
19440
19793
  this._safeAsync(this._onRawGroupV2MessageCreated(data));
19441
19794
  });
19442
- this._dispatcher.subscribe("_raw.group.v2.epoch_rotated", (data) => {
19443
- this._safeAsync(this._onV2EpochRotated(data));
19444
- });
19445
19795
  this._dispatcher.subscribe("_raw.group.v2.state_proposed", (data) => {
19446
19796
  this._safeAsync(this._onV2StateProposed(data));
19447
19797
  });
@@ -19741,7 +20091,18 @@ var _AUNClient = class _AUNClient {
19741
20091
  * 自动解密 message.pull/group.pull、Group E2EE 生命周期编排。
19742
20092
  */
19743
20093
  async call(method, params2) {
19744
- return await this._rpcPipeline.call(method, params2);
20094
+ try {
20095
+ return await this._rpcPipeline.call(method, params2);
20096
+ } catch (err) {
20097
+ await this._maybeHandleHalfOpenRpcFailure(err);
20098
+ throw err;
20099
+ }
20100
+ }
20101
+ async _maybeHandleHalfOpenRpcFailure(err) {
20102
+ if (this._closing || this.state !== "ready" /* READY */) return;
20103
+ if (!this._sessionOptions?.auto_reconnect || this._reconnectActive) return;
20104
+ if (!(err instanceof ConnectionError) && !(err instanceof TimeoutError)) return;
20105
+ await this._handleTransportDisconnect(err);
19745
20106
  }
19746
20107
  async createGroup(params2 = {}) {
19747
20108
  if (!isJsonObject(params2)) {
@@ -19794,8 +20155,30 @@ var _AUNClient = class _AUNClient {
19794
20155
  if (!store) {
19795
20156
  throw new ValidationError("bindGroupAid requires aidStore");
19796
20157
  }
19797
- const keyPair = await new CryptoProvider().generateIdentity();
20158
+ const groupId = String(params2.group_id ?? params2.group_aid ?? "").trim();
20159
+ const keystore = store._keystore;
20160
+ let keyPair = null;
20161
+ if (groupId && keystore && typeof keystore.loadPendingGroupBind === "function") {
20162
+ keyPair = await keystore.loadPendingGroupBind(groupId);
20163
+ }
20164
+ if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
20165
+ const generated = await new CryptoProvider().generateIdentity();
20166
+ keyPair = {
20167
+ private_key_pem: generated.private_key_pem,
20168
+ public_key_der_b64: generated.public_key_der_b64,
20169
+ curve: generated.curve
20170
+ };
20171
+ if (groupId && keystore && typeof keystore.savePendingGroupBind === "function") {
20172
+ await keystore.savePendingGroupBind(groupId, keyPair);
20173
+ }
20174
+ }
20175
+ if (!keyPair) {
20176
+ throw new ValidationError("bindGroupAid: failed to generate or load key pair");
20177
+ }
19798
20178
  const payload = { ...params2 };
20179
+ if (!String(payload.group_id ?? "").trim() && groupId) {
20180
+ payload.group_id = groupId;
20181
+ }
19799
20182
  payload.public_key = keyPair.public_key_der_b64;
19800
20183
  payload.curve = keyPair.curve;
19801
20184
  const result = await this.call("group.bind_group_aid", payload);
@@ -19816,6 +20199,94 @@ var _AUNClient = class _AUNClient {
19816
20199
  if (!imported.ok) {
19817
20200
  throw new ValidationError(imported.error.message);
19818
20201
  }
20202
+ if (groupId && keystore && typeof keystore.clearPendingGroupBind === "function") {
20203
+ await keystore.clearPendingGroupBind(groupId);
20204
+ }
20205
+ return result;
20206
+ }
20207
+ async renewGroupAid(params2 = {}, options = {}) {
20208
+ if (!isJsonObject(params2)) {
20209
+ throw new ValidationError("renewGroupAid params must be an object");
20210
+ }
20211
+ const store = options.aidStore;
20212
+ if (!store) {
20213
+ throw new ValidationError("renewGroupAid requires aidStore");
20214
+ }
20215
+ const groupId = String(params2.group_id ?? params2.group_aid ?? "").trim();
20216
+ if (!groupId) {
20217
+ throw new ValidationError("renewGroupAid requires group_id or group_aid");
20218
+ }
20219
+ let groupAid = String(params2.group_aid ?? "").trim();
20220
+ if (!groupAid) {
20221
+ const info = await this.call("group.get_info", { group_id: groupId, required: ["member"] });
20222
+ groupAid = String(info?.group_aid ?? "").trim();
20223
+ }
20224
+ if (!groupAid) {
20225
+ throw new ValidationError("renewGroupAid: unable to determine group_aid");
20226
+ }
20227
+ const loaded = await store.load(groupAid);
20228
+ if (!loaded.ok || !loaded.data) {
20229
+ throw new ValidationError(`renewGroupAid: group_aid identity not found: ${groupAid}`);
20230
+ }
20231
+ const oldAidObj = loaded.data.aid;
20232
+ if (!oldAidObj.isPrivateKeyValid() || !oldAidObj.privateKeyPem) {
20233
+ throw new ValidationError(`renewGroupAid: group_aid has no private key: ${groupAid}`);
20234
+ }
20235
+ const oldPublicKey = oldAidObj.publicKey;
20236
+ if (!oldPublicKey) {
20237
+ throw new ValidationError(`renewGroupAid: cannot determine old public key for ${groupAid}`);
20238
+ }
20239
+ const newKeyPair = await new CryptoProvider().generateIdentity();
20240
+ const newPublicKey = newKeyPair.public_key_der_b64;
20241
+ const newPrivateKey = newKeyPair.private_key_pem;
20242
+ const curve = newKeyPair.curve || "P-256";
20243
+ if (!newPublicKey || !newPrivateKey) {
20244
+ throw new ValidationError("renewGroupAid: generated incomplete group identity");
20245
+ }
20246
+ const nonce = globalThis.crypto.randomUUID().replace(/-/g, "");
20247
+ const issuedMs = Date.now();
20248
+ const oldHash = bytesToHex7(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(oldPublicKey))));
20249
+ const newHash = bytesToHex7(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(newPublicKey))));
20250
+ const canonical = [
20251
+ "aun-group-aid-renew-v1",
20252
+ groupId.toLowerCase(),
20253
+ groupAid.toLowerCase(),
20254
+ oldHash,
20255
+ newHash,
20256
+ nonce,
20257
+ String(issuedMs)
20258
+ ].join("|");
20259
+ const signed = await oldAidObj.sign(canonical);
20260
+ if (!signed.ok) {
20261
+ throw new ValidationError(`renewGroupAid: sign failed: ${signed.error.message}`);
20262
+ }
20263
+ const payload = { ...params2 };
20264
+ if (!String(payload.group_id ?? "").trim()) {
20265
+ payload.group_id = groupId;
20266
+ }
20267
+ payload.group_aid = groupAid;
20268
+ payload.old_public_key = oldPublicKey;
20269
+ payload.new_public_key = newPublicKey;
20270
+ payload.curve = curve;
20271
+ payload.renew_proof = { nonce, issued_ms: issuedMs, signature: signed.data.signature };
20272
+ const result = await this.call("group.renew_group_aid", payload);
20273
+ const resultMap = isJsonObject(result) ? result : {};
20274
+ const group = isJsonObject(resultMap.group) ? resultMap.group : {};
20275
+ const aidCert = isJsonObject(resultMap.aid_cert) ? resultMap.aid_cert : {};
20276
+ const returnedGroupAid = String(group.group_aid ?? resultMap.group_aid ?? groupAid).trim();
20277
+ const certPem = String(aidCert.cert ?? aidCert.cert_pem ?? resultMap.cert ?? resultMap.cert_pem ?? "").trim();
20278
+ if (!returnedGroupAid || !certPem) {
20279
+ throw new ValidationError("renewGroupAid response missing group.group_aid or aid_cert.cert");
20280
+ }
20281
+ const imported = await store.importGroupIdentity(returnedGroupAid, {
20282
+ private_key_pem: newPrivateKey,
20283
+ public_key_der_b64: newPublicKey,
20284
+ curve,
20285
+ cert_pem: certPem
20286
+ });
20287
+ if (!imported.ok) {
20288
+ throw new ValidationError(`renewGroupAid failed to persist group identity: ${imported.error.message}`);
20289
+ }
19819
20290
  return result;
19820
20291
  }
19821
20292
  async startGroupTransfer(params2 = {}, options = {}) {
@@ -19833,10 +20304,8 @@ var _AUNClient = class _AUNClient {
19833
20304
  }
19834
20305
  let groupAid = String(params2.group_aid ?? "").trim();
19835
20306
  if (!groupAid) {
19836
- const info = await this.call("group.get", { group_id: groupId });
19837
- const infoMap = isJsonObject(info) ? info : {};
19838
- const grp = isJsonObject(infoMap.group) ? infoMap.group : infoMap;
19839
- groupAid = String(grp.group_aid ?? "").trim();
20307
+ const info = await this.call("group.get_info", { group_id: groupId, required: ["member"] });
20308
+ groupAid = String(info?.group_aid ?? "").trim();
19840
20309
  }
19841
20310
  if (!groupAid) {
19842
20311
  throw new ValidationError("startGroupTransfer: unable to determine group_aid");
@@ -19883,10 +20352,8 @@ var _AUNClient = class _AUNClient {
19883
20352
  }
19884
20353
  let groupAid = String(params2.group_aid ?? "").trim();
19885
20354
  if (!groupAid) {
19886
- const info = await this.call("group.get", { group_id: groupId });
19887
- const infoMap = isJsonObject(info) ? info : {};
19888
- const grp = isJsonObject(infoMap.group) ? infoMap.group : infoMap;
19889
- groupAid = String(grp.group_aid ?? "").trim();
20355
+ const info = await this.call("group.get_info", { group_id: groupId, required: ["member"] });
20356
+ groupAid = String(info?.group_aid ?? "").trim();
19890
20357
  }
19891
20358
  if (!groupAid) {
19892
20359
  throw new ValidationError("completeGroupTransfer: unable to determine group_aid");
@@ -19974,7 +20441,7 @@ var _AUNClient = class _AUNClient {
19974
20441
  throw new ValidationError("notify payload is too large");
19975
20442
  }
19976
20443
  const targetAid = String(options.to ?? "").trim();
19977
- const targetGroupId = String(options.group_id ?? options.groupId ?? "").trim();
20444
+ const targetGroupId = String(options.group_aid ?? options.groupAid ?? options.group_id ?? options.groupId ?? "").trim();
19978
20445
  const targetDeviceId = String(options.device_id ?? options.deviceId ?? "").trim();
19979
20446
  const targetSlotId = String(options.slot_id ?? options.slotId ?? "").trim();
19980
20447
  const ttl = _AUNClient._normalizeNotifyTtl(options.ttl_ms ?? options.ttlMs);
@@ -19986,6 +20453,7 @@ var _AUNClient = class _AUNClient {
19986
20453
  }
19987
20454
  if (targetAid) {
19988
20455
  const eventMethod = _AUNClient._validateNotifyEventMethod(method);
20456
+ validateAIDFormat(targetAid, "notify.to");
19989
20457
  const target = { type: "aid", aid: targetAid };
19990
20458
  if (targetDeviceId) target.device_id = targetDeviceId;
19991
20459
  if (targetSlotId) target.slot_id = targetSlotId;
@@ -19999,11 +20467,13 @@ var _AUNClient = class _AUNClient {
19999
20467
  }
20000
20468
  if (targetGroupId) {
20001
20469
  const eventMethod = _AUNClient._validateNotifyEventMethod(method);
20470
+ validateGroupIDFormat(targetGroupId, "notify.group_id");
20002
20471
  const normalizedGroupId2 = normalizeGroupId(targetGroupId);
20003
20472
  if (!normalizedGroupId2) {
20004
20473
  throw new ValidationError("group_id is required for group notify");
20005
20474
  }
20006
20475
  const routeParams = {
20476
+ group_aid: normalizedGroupId2,
20007
20477
  group_id: normalizedGroupId2,
20008
20478
  deliver: { method: eventMethod, params: payload }
20009
20479
  };
@@ -20028,8 +20498,10 @@ var _AUNClient = class _AUNClient {
20028
20498
  delete p._skip_auto_ack;
20029
20499
  delete p.skip_auto_ack;
20030
20500
  delete p._group_cursor_params;
20031
- if (method.startsWith("group.") && p.group_id !== void 0 && p.group_id !== null) {
20032
- p.group_id = normalizeGroupId(String(p.group_id)) || String(p.group_id);
20501
+ if (method.startsWith("group.") && p.group_aid !== void 0 && p.group_aid !== null) {
20502
+ p.group_aid = normalizeGroupId(String(p.group_aid)) || String(p.group_aid);
20503
+ } else if (method.startsWith("group.") && p.group_id !== void 0 && p.group_id !== null) {
20504
+ p.group_aid = normalizeGroupId(String(p.group_id)) || String(p.group_id);
20033
20505
  }
20034
20506
  if (method.startsWith("group.") && p.device_id === void 0) {
20035
20507
  p.device_id = this._deviceId;
@@ -20135,17 +20607,17 @@ var _AUNClient = class _AUNClient {
20135
20607
  }
20136
20608
  _extractGroupIdFromResult(result) {
20137
20609
  const group = isJsonObject(result.group) ? result.group : null;
20138
- const gid = group ? String(group.group_id ?? "") : "";
20610
+ const gid = group ? String(group.group_aid ?? group.group_id ?? "") : "";
20139
20611
  if (gid) return gid;
20140
- const directGid = String(result.group_id ?? "");
20612
+ const directGid = String(result.group_aid ?? result.group_id ?? "");
20141
20613
  if (directGid) return directGid;
20142
20614
  const member = isJsonObject(result.member) ? result.member : null;
20143
- return member ? String(member.group_id ?? "") : "";
20615
+ return member ? String(member.group_aid ?? member.group_id ?? "") : "";
20144
20616
  }
20145
20617
  async _onRawGroupChanged(data) {
20146
20618
  const tStart = Date.now();
20147
20619
  const action = String(data?.action ?? "");
20148
- const groupIdInit = String(data?.group_id ?? "");
20620
+ const groupIdInit = String(data?.group_aid ?? data?.group_id ?? "");
20149
20621
  this._clientLog.debug(`_onRawGroupChanged enter: group_id=${groupIdInit} action=${action}`);
20150
20622
  try {
20151
20623
  if (isJsonObject(data)) {
@@ -20159,7 +20631,7 @@ var _AUNClient = class _AUNClient {
20159
20631
  d._verified = this._isEventSignatureVerified(verified);
20160
20632
  }
20161
20633
  }
20162
- const groupId = d.group_id ?? "";
20634
+ const groupId = d.group_aid ?? d.group_id ?? "";
20163
20635
  await this._delivery.handleGroupChangedEventSeq(d, groupId);
20164
20636
  } else {
20165
20637
  await this._dispatcher.publish("group.changed", data);
@@ -20188,18 +20660,22 @@ var _AUNClient = class _AUNClient {
20188
20660
  this._v2E2EE.deleteBootstrapCacheEntry(`group:${groupId}`);
20189
20661
  this._v2GroupSecurityLevels.delete(groupId);
20190
20662
  this._v2StateChains.delete(groupId);
20191
- this._seqTracker.removeNamespace(`group:${groupId}`);
20192
- this._seqTracker.removeNamespace(`group_event:${groupId}`);
20193
- this._saveSeqTrackerState();
20663
+ const groupNs = `group:${groupId}`;
20664
+ const groupEventNs = `group_event:${groupId}`;
20665
+ this._seqTracker.removeNamespace(groupNs);
20666
+ this._seqTracker.removeNamespace(groupEventNs);
20667
+ this._persistRepairedSeq(groupNs);
20668
+ this._persistRepairedSeq(groupEventNs);
20669
+ void this._saveSeqTrackerState();
20194
20670
  for (const key of this._gapFillDone.keys()) {
20195
20671
  if (key.includes(groupId)) {
20196
20672
  this._gapFillDone.delete(key);
20197
20673
  }
20198
20674
  }
20199
- this._pushedSeqs.delete(`group:${groupId}`);
20200
- this._pushedSeqs.delete(`group_event:${groupId}`);
20201
- this._pendingOrderedMsgs.delete(`group:${groupId}`);
20202
- this._pendingOrderedMsgs.delete(`group_event:${groupId}`);
20675
+ this._pushedSeqs.delete(groupNs);
20676
+ this._pushedSeqs.delete(groupEventNs);
20677
+ this._pendingOrderedMsgs.delete(groupNs);
20678
+ this._pendingOrderedMsgs.delete(groupEventNs);
20203
20679
  this._clientLog.info(`cleanup dissolved group ${groupId} local state`);
20204
20680
  }
20205
20681
  async _verifyEventSignature(_event, cs) {
@@ -20660,7 +21136,7 @@ var _AUNClient = class _AUNClient {
20660
21136
  let consecutiveFailures = 0;
20661
21137
  const maxFailures = 2;
20662
21138
  this._heartbeatTimer = setInterval(async () => {
20663
- if (this._state !== "connected" || this._closing) return;
21139
+ if (this.state !== "ready" /* READY */ || this._closing) return;
20664
21140
  try {
20665
21141
  const pong = await this._transport.call("meta.ping", {});
20666
21142
  this._heartbeatCount++;
@@ -20690,7 +21166,7 @@ var _AUNClient = class _AUNClient {
20690
21166
  clearInterval(this._heartbeatTimer);
20691
21167
  this._heartbeatTimer = null;
20692
21168
  }
20693
- if (newInterval > 0 && this._state === "connected" && !this._closing) {
21169
+ if (newInterval > 0 && this.state === "ready" /* READY */ && !this._closing) {
20694
21170
  this._startHeartbeat();
20695
21171
  }
20696
21172
  }
@@ -20704,7 +21180,7 @@ var _AUNClient = class _AUNClient {
20704
21180
  this._tokenRefreshTimer = globalThis.setTimeout(async () => {
20705
21181
  if (this._closing) return;
20706
21182
  this._tokenRefreshTimer = null;
20707
- if (this._state !== "connected" || !this._gatewayUrl) {
21183
+ if (this.state !== "ready" /* READY */ || !this._gatewayUrl) {
20708
21184
  scheduleRefresh();
20709
21185
  return;
20710
21186
  }
@@ -20722,13 +21198,13 @@ var _AUNClient = class _AUNClient {
20722
21198
  scheduleRefresh();
20723
21199
  return;
20724
21200
  }
20725
- if (this._state !== "connected" || !this._gatewayUrl || this._closing) {
21201
+ if (this.state !== "ready" /* READY */ || !this._gatewayUrl || this._closing) {
20726
21202
  scheduleRefresh();
20727
21203
  return;
20728
21204
  }
20729
21205
  try {
20730
21206
  identity = await this._auth.refreshCachedTokens(this._gatewayUrl, identity);
20731
- if (this._state !== "connected") {
21207
+ if (this.state !== "ready" /* READY */) {
20732
21208
  scheduleRefresh();
20733
21209
  return;
20734
21210
  }
@@ -20805,6 +21281,9 @@ var _AUNClient = class _AUNClient {
20805
21281
  if (this._closing || this._state === "closed") return;
20806
21282
  this._state = "standby";
20807
21283
  this._stopBackgroundTasks();
21284
+ void this._transport.close().catch((exc) => {
21285
+ this._clientLog.debug(`transport cleanup skipped: ${formatCaughtError2(exc)}`);
21286
+ });
20808
21287
  await this._dispatcher.publish("state_change", {
20809
21288
  state: this._publicState(this._state),
20810
21289
  error
@@ -20972,6 +21451,7 @@ var _AUNClient = class _AUNClient {
20972
21451
  return JSON.stringify([this._aid, this._deviceId, this._slotId]);
20973
21452
  }
20974
21453
  _resetSeqTrackingState() {
21454
+ void this._saveSeqTrackerState();
20975
21455
  this._resetV2IdentityRuntime();
20976
21456
  this._seqTracker = new SeqTracker();
20977
21457
  this._seqTrackerContext = null;
@@ -20991,6 +21471,7 @@ var _AUNClient = class _AUNClient {
20991
21471
  _refreshSeqTrackerContext() {
20992
21472
  const nextContext = this._currentSeqTrackerContext();
20993
21473
  if (nextContext === this._seqTrackerContext) return;
21474
+ void this._saveSeqTrackerState();
20994
21475
  this._seqTracker = new SeqTracker();
20995
21476
  this._gapFillDone.clear();
20996
21477
  this._pushedSeqs.clear();
@@ -21010,6 +21491,9 @@ var _AUNClient = class _AUNClient {
21010
21491
  _saveSeqTrackerState() {
21011
21492
  return this._delivery.saveSeqTrackerState();
21012
21493
  }
21494
+ _persistSeq(ns, forceSeq) {
21495
+ return this._delivery.persistSeq(ns, forceSeq);
21496
+ }
21013
21497
  _persistRepairedSeq(ns) {
21014
21498
  return this._delivery.persistRepairedSeq(ns);
21015
21499
  }
@@ -21502,12 +21986,6 @@ var _AUNClient = class _AUNClient {
21502
21986
  async _onV2PushNotification(data) {
21503
21987
  return this._delivery.onV2PushNotification(data);
21504
21988
  }
21505
- /**
21506
- * 处理 V2 epoch 轮换事件:清除 bootstrap 缓存 + 触发 SPK rotation。
21507
- */
21508
- async _onV2EpochRotated(data) {
21509
- return this._v2E2EE.handleV2EpochRotated(data);
21510
- }
21511
21989
  /** 安全执行异步操作(不阻塞调用方,错误打 warning 便于排障) */
21512
21990
  _safeAsync(promise) {
21513
21991
  promise.catch((exc) => {
@@ -22826,11 +23304,6 @@ export {
22826
23304
  E2EEDecryptFailedError,
22827
23305
  E2EEDegradedError,
22828
23306
  E2EEError,
22829
- E2EEGroupCommitmentInvalidError,
22830
- E2EEGroupDecryptFailedError,
22831
- E2EEGroupEpochMismatchError,
22832
- E2EEGroupNotMemberError,
22833
- E2EEGroupSecretMissingError,
22834
23307
  EmbeddedServiceRegistry,
22835
23308
  EndpointPolicy,
22836
23309
  EventDispatcher,
@@ -22876,7 +23349,9 @@ export {
22876
23349
  V2Session,
22877
23350
  ValidationError,
22878
23351
  VERSION as __version__,
23352
+ buildDiscoveryHost,
22879
23353
  computeStateCommitment,
23354
+ convertToGroupAid,
22880
23355
  createConfig,
22881
23356
  createDefaultSecretStore,
22882
23357
  decryptMessage,
@@ -22887,8 +23362,14 @@ export {
22887
23362
  isJsonObject,
22888
23363
  mapCollabError,
22889
23364
  mapRemoteError,
23365
+ normalizeGroupAid,
23366
+ normalizeGroupId,
22890
23367
  resultErr,
22891
- resultOk
23368
+ resultOk,
23369
+ splitGroupId,
23370
+ validateAIDFormat,
23371
+ validateGroupAIDFormat,
23372
+ validateGroupIDFormat
22892
23373
  };
22893
23374
  /*! Bundled license information:
22894
23375