@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.
- package/CHANGELOG.md +550 -464
- package/_packed_docs/CHANGELOG-validators.md +147 -0
- package/_packed_docs/CHANGELOG.md +550 -464
- package/_packed_docs/INDEX.md +201 -177
- package/_packed_docs/KITE_DOCS_GUIDE.md +38 -32
- 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
- package/_packed_docs/aun-perf-audit-critical-bugs.md +315 -0
- package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +263 -260
- package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +331 -0
- package/_packed_docs/protocol/00-/346/200/273/350/247/210/344/270/216/345/210/206/345/261/202.md +2 -2
- 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
- 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
- package/_packed_docs/protocol/03-Gateway-/350/277/236/346/216/245/346/250/241/345/274/217.md +8 -5
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +18 -19
- 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
- package/_packed_docs/protocol/08-AUN-E2EE-Group.md +139 -746
- package/_packed_docs/protocol/08-AUN-E2EE.md +12 -10
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +117 -171
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +6 -0
- 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
- package/_packed_docs/protocol/16-/347/263/273/347/273/237/347/233/256/345/275/225/344/277/235/346/212/244/346/226/271/346/241/210.md +177 -0
- package/_packed_docs/protocol/README.md +5 -4
- package/_packed_docs/protocol/aun-docs-guide.md +2 -2
- package/_packed_docs/protocol/index.md +12 -7
- 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
- package/_packed_docs/protocol//351/231/204/345/275/225A-/346/234/257/350/257/255/350/241/250.md +13 -13
- 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
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +15 -13
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +26 -18
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +35 -284
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +473 -442
- package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +11 -7
- package/_packed_docs/sdk/09-collab-rpc-manual.md +581 -550
- package/_packed_docs/sdk/09-group-rpc-manual.md +367 -433
- package/_packed_docs/sdk/09-message-rpc-manual.md +50 -28
- package/_packed_docs/sdk/09-payload-reference.md +3 -3
- package/_packed_docs/sdk/09-storage-rpc-manual.md +57 -20
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +18 -17
- 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
- package/_packed_docs/sdk/INDEX.md +25 -24
- package/_packed_docs/sdk/Notify/351/200/232/347/237/245/346/226/271/346/241/210.md +6 -2
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +18 -9
- package/dist/agent-md.js.map +1 -1
- package/dist/bundle.js +1661 -1180
- package/dist/client/delivery.d.ts +13 -2
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +251 -46
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.d.ts.map +1 -1
- package/dist/client/group-state.js +36 -14
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/lifecycle.js +2 -2
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +1 -0
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +166 -50
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +14 -1
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +376 -126
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +5 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +187 -46
- package/dist/client.js.map +1 -1
- package/dist/collab/client.d.ts +8 -0
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +12 -0
- package/dist/collab/client.js.map +1 -1
- package/dist/errors.d.ts +0 -20
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +8 -51
- package/dist/errors.js.map +1 -1
- package/dist/facades.d.ts +9 -4
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +192 -31
- package/dist/facades.js.map +1 -1
- package/dist/group-fs.d.ts +17 -0
- package/dist/group-fs.d.ts.map +1 -1
- package/dist/group-fs.js +54 -11
- package/dist/group-fs.js.map +1 -1
- package/dist/group-id.d.ts +9 -12
- package/dist/group-id.d.ts.map +1 -1
- package/dist/group-id.js +41 -63
- package/dist/group-id.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +2 -54
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.d.ts +3 -0
- package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.js +65 -0
- package/dist/keystore/indexeddb-identity-store.js.map +1 -1
- package/dist/keystore/indexeddb-shared.d.ts +3 -17
- package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
- package/dist/keystore/indexeddb-shared.js +4 -47
- package/dist/keystore/indexeddb-shared.js.map +1 -1
- package/dist/keystore/indexeddb-token-store.d.ts +1 -64
- package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-token-store.js +45 -774
- package/dist/keystore/indexeddb-token-store.js.map +1 -1
- package/dist/logger.d.ts +2 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +4 -0
- package/dist/logger.js.map +1 -1
- package/dist/storage/lowlevel.d.ts +9 -1
- package/dist/storage/lowlevel.d.ts.map +1 -1
- package/dist/storage/lowlevel.js +12 -1
- package/dist/storage/lowlevel.js.map +1 -1
- package/dist/storage/vfs.d.ts +22 -0
- package/dist/storage/vfs.d.ts.map +1 -1
- package/dist/storage/vfs.js +54 -0
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +336 -49
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +2 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +96 -3
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +39 -56
- package/dist/types.d.ts.map +1 -1
- package/dist/v2/session/session.d.ts +2 -0
- package/dist/v2/session/session.d.ts.map +1 -1
- package/dist/v2/session/session.js +58 -22
- package/dist/v2/session/session.js.map +1 -1
- package/dist/v2/state/commitment.d.ts +1 -1
- package/dist/v2/state/commitment.d.ts.map +1 -1
- package/dist/v2/state/commitment.js +5 -3
- package/dist/v2/state/commitment.js.map +1 -1
- package/dist/validators.d.ts +35 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +127 -0
- package/dist/validators.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/_packed_docs/collab-gateway-boundary-test-report.md +0 -164
package/dist/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.
|
|
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
|
|
883
|
-
|
|
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
|
|
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
|
|
894
|
-
const
|
|
895
|
-
if (
|
|
896
|
-
const merged =
|
|
897
|
-
return
|
|
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
|
-
|
|
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)
|
|
907
|
-
const domain = value.slice(atIdx + 1)
|
|
908
|
-
if (base && domain) return
|
|
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
|
-
|
|
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
|
-
|
|
926
|
-
|
|
927
|
-
|
|
895
|
+
return {
|
|
896
|
+
base: trimDots(groupAid.slice(0, dotIdx)),
|
|
897
|
+
domain: trimDots(groupAid.slice(dotIdx + 1))
|
|
898
|
+
};
|
|
928
899
|
}
|
|
929
|
-
|
|
930
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
1922
|
+
var STORE_PENDING_BINDS = "pending_binds";
|
|
1846
1923
|
var DB_NAME = "aun-keystore";
|
|
1847
|
-
var DB_VERSION =
|
|
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
|
-
|
|
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
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
group_id:
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
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 (
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
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
|
-
|
|
6598
|
+
ws.send(payload);
|
|
7115
6599
|
} catch (err) {
|
|
7116
|
-
throw
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
8585
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
8670
|
+
const context = this.currentSeqTrackerContext();
|
|
8671
|
+
if (!context) {
|
|
8672
|
+
await this.flushSeqTrackerPending();
|
|
8673
|
+
return;
|
|
8674
|
+
}
|
|
9066
8675
|
const state = client._seqTracker.exportState();
|
|
9067
|
-
|
|
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
|
-
|
|
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(
|
|
9087
|
-
current.seq_tracker_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
|
|
9104
|
-
device_id:
|
|
9105
|
-
slot_id:
|
|
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.
|
|
9686
|
-
"group.update_avatar",
|
|
9443
|
+
"group.set_settings",
|
|
9687
9444
|
"group.update_announcement",
|
|
9688
|
-
"group.
|
|
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
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
9857
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
9982
|
-
|
|
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"
|
|
9991
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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
|
|
11034
|
-
const
|
|
11035
|
-
const
|
|
11036
|
-
const srcRemote = typeof src === "string" && isGroupRemoteCopyPath(src,
|
|
11037
|
-
const dstRemote = typeof dst === "string" && isGroupRemoteCopyPath(dst,
|
|
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
|
|
11045
|
-
const
|
|
11046
|
-
const
|
|
11047
|
-
if (!isGroupRemoteCopyPath(src,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11323
|
-
return this.call("group.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
16159
|
-
|
|
16160
|
-
|
|
16161
|
-
|
|
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
|
|
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
|
|
16257
|
-
const
|
|
16258
|
-
|
|
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
|
|
16269
|
-
const
|
|
16270
|
-
|
|
16271
|
-
|
|
16272
|
-
|
|
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
|
|
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
|
|
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:
|
|
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,
|
|
16316
|
-
this._lastUploadedGroupSPKIds.set(
|
|
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
|
-
|
|
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.
|
|
17183
|
+
client._persistSeq(ns);
|
|
16939
17184
|
}
|
|
16940
|
-
const ackNeeded =
|
|
17185
|
+
const ackNeeded = pageMessages.length > 0 && ackSeq > 0 && ackSeq > lastAutoAckSeq && (contigAdvanced || hasServerAckSeq && ackSeq > serverAckSeq);
|
|
16941
17186
|
if (ackNeeded) {
|
|
16942
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
17432
|
+
client._persistSeq(ns);
|
|
17156
17433
|
}
|
|
17157
17434
|
const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, "current_seq");
|
|
17158
|
-
const ackNeeded =
|
|
17435
|
+
const ackNeeded = pageMessages.length > 0 && ackSeq > 0 && ackSeq > lastAutoAckSeq && ownsCursor && (contigAdvanced || hasServerCursor && ackSeq > cursorCurrentSeq);
|
|
17159
17436
|
if (ackNeeded) {
|
|
17160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17669
|
-
|
|
17670
|
-
|
|
17671
|
-
|
|
17672
|
-
|
|
17673
|
-
|
|
17674
|
-
|
|
17675
|
-
|
|
17676
|
-
|
|
17677
|
-
|
|
17678
|
-
|
|
17679
|
-
|
|
17680
|
-
|
|
17681
|
-
|
|
17682
|
-
|
|
17683
|
-
|
|
17684
|
-
|
|
17685
|
-
|
|
17686
|
-
|
|
17687
|
-
|
|
17688
|
-
|
|
17689
|
-
|
|
17690
|
-
|
|
17691
|
-
|
|
17692
|
-
|
|
17693
|
-
|
|
17694
|
-
|
|
17695
|
-
|
|
17696
|
-
|
|
17697
|
-
const
|
|
17698
|
-
|
|
17699
|
-
|
|
17700
|
-
|
|
17701
|
-
|
|
17702
|
-
|
|
17703
|
-
|
|
17704
|
-
|
|
17705
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
17794
|
-
|
|
17795
|
-
|
|
17796
|
-
|
|
17797
|
-
|
|
17798
|
-
const
|
|
17799
|
-
|
|
17800
|
-
dev
|
|
17801
|
-
|
|
17802
|
-
|
|
17803
|
-
|
|
17804
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
18464
|
+
const rawGroupId = data.group_aid ?? data.group_id;
|
|
18465
|
+
const groupId = normalizedGroupId(rawGroupId);
|
|
18129
18466
|
if (groupId) {
|
|
18130
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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.
|
|
18757
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
19837
|
-
|
|
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.
|
|
19887
|
-
|
|
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.
|
|
20032
|
-
p.
|
|
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
|
-
|
|
20192
|
-
|
|
20193
|
-
this.
|
|
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(
|
|
20200
|
-
this._pushedSeqs.delete(
|
|
20201
|
-
this._pendingOrderedMsgs.delete(
|
|
20202
|
-
this._pendingOrderedMsgs.delete(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|