@palbase/web 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{analytics-facade-DV2uAiSg.d.cts → analytics-facade-CAKBIH_U.d.cts} +351 -5
- package/dist/{analytics-facade-CE_vGf1F.d.ts → analytics-facade-DLH-KivI.d.ts} +351 -5
- package/dist/chunk-XVLR3HGD.js +6843 -0
- package/dist/chunk-XVLR3HGD.js.map +1 -0
- package/dist/index.cjs +3617 -187
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +207 -4
- package/dist/index.d.ts +207 -4
- package/dist/index.js +7 -1
- package/dist/internal.cjs +3613 -167
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +4 -3
- package/dist/internal.d.ts +4 -3
- package/dist/internal.js +1 -1
- package/dist/next/client.cjs +3611 -177
- package/dist/next/client.cjs.map +1 -1
- package/dist/next/client.js +1 -1
- package/dist/next/index.cjs +3616 -170
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +3 -8
- package/dist/next/index.d.ts +3 -8
- package/dist/next/index.js +14 -5
- package/dist/next/index.js.map +1 -1
- package/dist/{pb-EMn_MF9g.d.ts → pb-DioxNuEV.d.cts} +3 -1
- package/dist/{pb-DcknUtCh.d.cts → pb-HegMSSk-.d.ts} +3 -1
- package/dist/pkg/palbe_mls_bg.wasm +0 -0
- package/dist/react/index.cjs +97 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +28 -2
- package/dist/react/index.d.ts +28 -2
- package/dist/react/index.js +86 -1
- package/dist/react/index.js.map +1 -1
- package/package.json +6 -3
- package/dist/chunk-AVEXGXRQ.js +0 -3384
- package/dist/chunk-AVEXGXRQ.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -23,10 +23,13 @@ __export(index_exports, {
|
|
|
23
23
|
BackendError: () => BackendError,
|
|
24
24
|
PalbeAnalytics: () => PalbeAnalytics,
|
|
25
25
|
PalbeAuth: () => PalbeAuth,
|
|
26
|
+
PalbeCalls: () => PalbeCalls,
|
|
26
27
|
PalbeFlags: () => PalbeFlags,
|
|
28
|
+
PalbeMessaging: () => PalbeMessaging,
|
|
27
29
|
PalbeRealtime: () => PalbeRealtime,
|
|
28
30
|
RealtimeChannel: () => RealtimeChannel,
|
|
29
31
|
VERSION: () => VERSION,
|
|
32
|
+
initMls: () => initMls,
|
|
30
33
|
isBackendError: () => isBackendError,
|
|
31
34
|
localStorageSessionStorage: () => localStorageSessionStorage,
|
|
32
35
|
memorySessionStorage: () => memorySessionStorage,
|
|
@@ -35,9 +38,10 @@ __export(index_exports, {
|
|
|
35
38
|
module.exports = __toCommonJS(index_exports);
|
|
36
39
|
|
|
37
40
|
// src/api-key.ts
|
|
41
|
+
var API_KEY_RE = /^pb_([^_]+)_[cs][A-Za-z0-9]{20}$/;
|
|
38
42
|
function endpointRefFromApiKey(apiKey) {
|
|
39
|
-
const
|
|
40
|
-
return
|
|
43
|
+
const m = API_KEY_RE.exec(apiKey);
|
|
44
|
+
return m ? m[1] ?? "" : "";
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
// ../core/dist/index.js
|
|
@@ -696,6 +700,220 @@ var PalbeAuth = class {
|
|
|
696
700
|
}
|
|
697
701
|
};
|
|
698
702
|
|
|
703
|
+
// src/calls/facade.ts
|
|
704
|
+
var import_livekit_client = require("livekit-client");
|
|
705
|
+
var Call = class {
|
|
706
|
+
id;
|
|
707
|
+
_state = "connecting";
|
|
708
|
+
_participants = /* @__PURE__ */ new Map();
|
|
709
|
+
_listeners = /* @__PURE__ */ new Set();
|
|
710
|
+
_room;
|
|
711
|
+
_keyProvider;
|
|
712
|
+
/** @internal — obtain via PalbeCalls.start() / .accept() */
|
|
713
|
+
constructor(callId, room, keyProvider) {
|
|
714
|
+
this.id = callId;
|
|
715
|
+
this._room = room;
|
|
716
|
+
this._keyProvider = keyProvider;
|
|
717
|
+
this._wireRoomEvents();
|
|
718
|
+
}
|
|
719
|
+
// ─── State ──────────────────────────────────────────────────────────────
|
|
720
|
+
get state() {
|
|
721
|
+
return this._state;
|
|
722
|
+
}
|
|
723
|
+
/** Snapshot of current remote participants (does NOT include the local participant). */
|
|
724
|
+
get participants() {
|
|
725
|
+
return Array.from(this._participants.values());
|
|
726
|
+
}
|
|
727
|
+
get localParticipant() {
|
|
728
|
+
return this._room.localParticipant;
|
|
729
|
+
}
|
|
730
|
+
// ─── Subscribe ──────────────────────────────────────────────────────────
|
|
731
|
+
/**
|
|
732
|
+
* Subscribe to call changes (state + participant updates).
|
|
733
|
+
* Returns an `Unsubscribe` function — matches the SDK's flags/auth pattern.
|
|
734
|
+
*/
|
|
735
|
+
subscribe(callback) {
|
|
736
|
+
this._listeners.add(callback);
|
|
737
|
+
return () => {
|
|
738
|
+
this._listeners.delete(callback);
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
// ─── Controls ───────────────────────────────────────────────────────────
|
|
742
|
+
/** Mute / unmute the local microphone. */
|
|
743
|
+
async mute(on) {
|
|
744
|
+
await this._room.localParticipant.setMicrophoneEnabled(!on);
|
|
745
|
+
}
|
|
746
|
+
/** Enable / disable the local camera. */
|
|
747
|
+
async setCamera(on) {
|
|
748
|
+
await this._room.localParticipant.setCameraEnabled(on);
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Set a raw frame encryption key (ArrayBuffer) for this call.
|
|
752
|
+
* Design seam for SP-1.x web-MLS: call this with the MLS exporter secret
|
|
753
|
+
* to enable frame-level E2EE via ExternalE2EEKeyProvider.
|
|
754
|
+
*
|
|
755
|
+
* NOTE: The room must have been opened with `e2ee` options for this to take
|
|
756
|
+
* effect. Currently calls connect WITHOUT e2ee — see module docstring.
|
|
757
|
+
*
|
|
758
|
+
* TODO(SP-1-web): wire this to MLS-derived key on session join.
|
|
759
|
+
*/
|
|
760
|
+
async setMediaKey(key) {
|
|
761
|
+
await this._keyProvider.setKey(key);
|
|
762
|
+
}
|
|
763
|
+
/** Leave the call and disconnect from the LiveKit room. */
|
|
764
|
+
async leave() {
|
|
765
|
+
await this._room.disconnect();
|
|
766
|
+
this._setState("ended");
|
|
767
|
+
}
|
|
768
|
+
// ─── Internal ───────────────────────────────────────────────────────────
|
|
769
|
+
_setState(next) {
|
|
770
|
+
if (this._state === next) return;
|
|
771
|
+
this._state = next;
|
|
772
|
+
this._emit();
|
|
773
|
+
}
|
|
774
|
+
_emit() {
|
|
775
|
+
for (const cb of this._listeners) cb(this);
|
|
776
|
+
}
|
|
777
|
+
_upsertParticipant(p) {
|
|
778
|
+
const existing = this._participants.get(p.identity);
|
|
779
|
+
const entry = {
|
|
780
|
+
identity: p.identity,
|
|
781
|
+
isSpeaking: p.isSpeaking,
|
|
782
|
+
audioMuted: !p.isMicrophoneEnabled,
|
|
783
|
+
videoEnabled: p.isCameraEnabled,
|
|
784
|
+
videoElement: existing?.videoElement ?? null,
|
|
785
|
+
audioElement: existing?.audioElement ?? null
|
|
786
|
+
};
|
|
787
|
+
this._participants.set(p.identity, entry);
|
|
788
|
+
}
|
|
789
|
+
_attachTrack(track, participant) {
|
|
790
|
+
const element = track.attach();
|
|
791
|
+
const entry = this._participants.get(participant.identity);
|
|
792
|
+
if (track.kind === import_livekit_client.Track.Kind.Video) {
|
|
793
|
+
if (entry) {
|
|
794
|
+
entry.videoElement = element;
|
|
795
|
+
}
|
|
796
|
+
} else if (track.kind === import_livekit_client.Track.Kind.Audio) {
|
|
797
|
+
if (entry) {
|
|
798
|
+
entry.audioElement = element;
|
|
799
|
+
element.play().catch(() => {
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
this._emit();
|
|
804
|
+
}
|
|
805
|
+
_wireRoomEvents() {
|
|
806
|
+
this._room.on(import_livekit_client.RoomEvent.Connected, () => {
|
|
807
|
+
this._setState("active");
|
|
808
|
+
}).on(
|
|
809
|
+
import_livekit_client.RoomEvent.TrackSubscribed,
|
|
810
|
+
(track, _pub, participant) => {
|
|
811
|
+
this._upsertParticipant(participant);
|
|
812
|
+
this._attachTrack(track, participant);
|
|
813
|
+
}
|
|
814
|
+
).on(
|
|
815
|
+
import_livekit_client.RoomEvent.TrackUnsubscribed,
|
|
816
|
+
(_track, _pub, participant) => {
|
|
817
|
+
this._upsertParticipant(participant);
|
|
818
|
+
this._emit();
|
|
819
|
+
}
|
|
820
|
+
).on(import_livekit_client.RoomEvent.ParticipantConnected, (participant) => {
|
|
821
|
+
this._upsertParticipant(participant);
|
|
822
|
+
this._emit();
|
|
823
|
+
}).on(import_livekit_client.RoomEvent.ParticipantDisconnected, (participant) => {
|
|
824
|
+
this._participants.delete(participant.identity);
|
|
825
|
+
this._emit();
|
|
826
|
+
}).on(import_livekit_client.RoomEvent.ActiveSpeakersChanged, (speakers) => {
|
|
827
|
+
for (const [id, entry] of this._participants) {
|
|
828
|
+
entry.isSpeaking = speakers.some((s) => s.identity === id);
|
|
829
|
+
}
|
|
830
|
+
this._emit();
|
|
831
|
+
}).on(import_livekit_client.RoomEvent.Disconnected, () => {
|
|
832
|
+
this._setState("ended");
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
var PalbeCalls = class {
|
|
837
|
+
rt;
|
|
838
|
+
constructor(rt) {
|
|
839
|
+
this.rt = rt;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Start a new call in a messaging group.
|
|
843
|
+
*
|
|
844
|
+
* @param groupId - Messaging group display-id (`grp_<uuidv7>`)
|
|
845
|
+
* @param media - Which media tracks to publish (default: audio + video)
|
|
846
|
+
* @returns A `Call` in `connecting` state; transitions to `active` once LiveKit connects.
|
|
847
|
+
*
|
|
848
|
+
* @throws BackendError('network', { code: 'call_service_unavailable' }) if LiveKit SFU is down (503)
|
|
849
|
+
* @throws BackendError('forbidden', { code: 'not_group_member' }) if the caller is not a member (403)
|
|
850
|
+
* @throws BackendError('conflict', { code: 'call_room_full' }) if the room is full (409)
|
|
851
|
+
*/
|
|
852
|
+
async start(groupId, { media = ["audio", "video"] } = {}) {
|
|
853
|
+
if (!groupId) {
|
|
854
|
+
throw new BackendError("validation", {
|
|
855
|
+
code: "invalid_group_id",
|
|
856
|
+
message: "groupId must be a non-empty string"
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
const res = await palbeRequest(
|
|
860
|
+
this.rt,
|
|
861
|
+
"POST",
|
|
862
|
+
`/v1/messaging/groups/${encodeURIComponent(groupId)}/calls`,
|
|
863
|
+
{ body: { media } }
|
|
864
|
+
);
|
|
865
|
+
return this._connect(res.call_id, res.token, res.edge_url, media);
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Accept an incoming call in a messaging group.
|
|
869
|
+
*
|
|
870
|
+
* @param groupId - Messaging group display-id
|
|
871
|
+
* @param callId - Call identifier (from the push notification payload)
|
|
872
|
+
* @param media - Which media tracks to publish (default: audio + video)
|
|
873
|
+
*/
|
|
874
|
+
async accept(groupId, callId, { media = ["audio", "video"] } = {}) {
|
|
875
|
+
const res = await palbeRequest(
|
|
876
|
+
this.rt,
|
|
877
|
+
"POST",
|
|
878
|
+
`/v1/messaging/groups/${encodeURIComponent(groupId)}/calls/${encodeURIComponent(callId)}/accept`,
|
|
879
|
+
{ body: {} }
|
|
880
|
+
);
|
|
881
|
+
return this._connect(callId, res.token, res.edge_url, media);
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Decline an incoming call.
|
|
885
|
+
*
|
|
886
|
+
* @param groupId - Messaging group display-id
|
|
887
|
+
* @param callId - Call identifier
|
|
888
|
+
* @param reason - Optional decline reason string
|
|
889
|
+
*/
|
|
890
|
+
async decline(groupId, callId, reason) {
|
|
891
|
+
await palbeRequest(
|
|
892
|
+
this.rt,
|
|
893
|
+
"POST",
|
|
894
|
+
`/v1/messaging/groups/${encodeURIComponent(groupId)}/calls/${encodeURIComponent(callId)}/decline`,
|
|
895
|
+
{ body: { reason: reason ?? "" } }
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
// ─── Internal ─────────────────────────────────────────────────────────
|
|
899
|
+
async _connect(callId, token, edgeUrl, media) {
|
|
900
|
+
const keyProvider = new import_livekit_client.ExternalE2EEKeyProvider();
|
|
901
|
+
const room = new import_livekit_client.Room({
|
|
902
|
+
adaptiveStream: true,
|
|
903
|
+
dynacast: true
|
|
904
|
+
});
|
|
905
|
+
const call = new Call(callId, room, keyProvider);
|
|
906
|
+
await room.connect(edgeUrl, token);
|
|
907
|
+
if (media.includes("audio")) {
|
|
908
|
+
await room.localParticipant.setMicrophoneEnabled(true);
|
|
909
|
+
}
|
|
910
|
+
if (media.includes("video")) {
|
|
911
|
+
await room.localParticipant.setCameraEnabled(true);
|
|
912
|
+
}
|
|
913
|
+
return call;
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
|
|
699
917
|
// ../modules/flags/dist/index.js
|
|
700
918
|
var FLAG_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
701
919
|
var FlagsClient = class {
|
|
@@ -1174,218 +1392,3421 @@ function parsePersisted(raw) {
|
|
|
1174
1392
|
syncVersion: rec.syncVersion
|
|
1175
1393
|
};
|
|
1176
1394
|
}
|
|
1177
|
-
function defaultEnv() {
|
|
1178
|
-
const storage = resolveStorage();
|
|
1179
|
-
const visibility = resolveVisibility();
|
|
1180
|
-
return {
|
|
1181
|
-
now: () => Date.now(),
|
|
1182
|
-
setInterval: (handler, ms) => setInterval(handler, ms),
|
|
1183
|
-
clearInterval: (handle) => clearInterval(handle),
|
|
1184
|
-
storage,
|
|
1185
|
-
visibility
|
|
1186
|
-
};
|
|
1395
|
+
function defaultEnv() {
|
|
1396
|
+
const storage = resolveStorage();
|
|
1397
|
+
const visibility = resolveVisibility();
|
|
1398
|
+
return {
|
|
1399
|
+
now: () => Date.now(),
|
|
1400
|
+
setInterval: (handler, ms) => setInterval(handler, ms),
|
|
1401
|
+
clearInterval: (handle) => clearInterval(handle),
|
|
1402
|
+
storage,
|
|
1403
|
+
visibility
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
function resolveStorage() {
|
|
1407
|
+
try {
|
|
1408
|
+
if (typeof localStorage === "undefined") {
|
|
1409
|
+
return null;
|
|
1410
|
+
}
|
|
1411
|
+
const probe = "__palbase_flags_probe__";
|
|
1412
|
+
localStorage.setItem(probe, "1");
|
|
1413
|
+
localStorage.removeItem(probe);
|
|
1414
|
+
return localStorage;
|
|
1415
|
+
} catch {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
function resolveVisibility() {
|
|
1420
|
+
if (typeof document === "undefined") {
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
const doc = document;
|
|
1424
|
+
return {
|
|
1425
|
+
isHidden: () => doc.visibilityState === "hidden",
|
|
1426
|
+
onVisibilityChange: (cb) => {
|
|
1427
|
+
doc.addEventListener("visibilitychange", cb);
|
|
1428
|
+
return () => doc.removeEventListener("visibilitychange", cb);
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// src/flags-facade.ts
|
|
1434
|
+
function palbeAuthAdapter(auth) {
|
|
1435
|
+
return {
|
|
1436
|
+
onAuthStateChange(callback) {
|
|
1437
|
+
const unsubscribe = auth.onAuthStateChange(() => {
|
|
1438
|
+
const signedIn = auth.isSignedIn;
|
|
1439
|
+
callback(signedIn ? "SIGNED_IN" : "SIGNED_OUT", signedIn ? { signedIn } : null);
|
|
1440
|
+
});
|
|
1441
|
+
return { data: { subscription: { unsubscribe } } };
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
function serverPoolEnv() {
|
|
1446
|
+
return {
|
|
1447
|
+
now: () => Date.now(),
|
|
1448
|
+
setInterval: () => {
|
|
1449
|
+
throw new Error("palbe flags: polling is disabled server-side");
|
|
1450
|
+
},
|
|
1451
|
+
clearInterval: () => {
|
|
1452
|
+
},
|
|
1453
|
+
storage: null,
|
|
1454
|
+
visibility: null
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
function sameFlagValue(a, b) {
|
|
1458
|
+
if (Object.is(a, b)) return true;
|
|
1459
|
+
if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) {
|
|
1460
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1461
|
+
}
|
|
1462
|
+
return false;
|
|
1463
|
+
}
|
|
1464
|
+
var PalbeFlags = class {
|
|
1465
|
+
transport;
|
|
1466
|
+
pool;
|
|
1467
|
+
constructor(rt) {
|
|
1468
|
+
this.transport = new FlagsClient(rt.http);
|
|
1469
|
+
const browser = typeof document !== "undefined";
|
|
1470
|
+
const ref = endpointRefFromApiKey(rt.config.apiKey);
|
|
1471
|
+
const options = {
|
|
1472
|
+
auth: palbeAuthAdapter(rt.auth),
|
|
1473
|
+
...ref ? { storageKey: `palbe.flags.${ref}` } : {},
|
|
1474
|
+
...browser ? {} : { pollIntervalMs: 0, env: serverPoolEnv() }
|
|
1475
|
+
};
|
|
1476
|
+
this.pool = new FlagsPool(this.transport, options);
|
|
1477
|
+
if (browser) this.pool.start();
|
|
1478
|
+
}
|
|
1479
|
+
/** Resolves once the first snapshot (or persisted hydrate) is available. Auto-starts the pool. */
|
|
1480
|
+
ready() {
|
|
1481
|
+
return this.pool.ready();
|
|
1482
|
+
}
|
|
1483
|
+
/** Force an immediate re-snapshot. */
|
|
1484
|
+
refresh() {
|
|
1485
|
+
return this.pool.refresh();
|
|
1486
|
+
}
|
|
1487
|
+
/** Frozen snapshot of all cached values (identity-stable until a change). */
|
|
1488
|
+
all() {
|
|
1489
|
+
return this.pool.all();
|
|
1490
|
+
}
|
|
1491
|
+
/** Raw cached value for `key`, or `undefined` when not in the cache. */
|
|
1492
|
+
get(key) {
|
|
1493
|
+
return this.pool.all()[key];
|
|
1494
|
+
}
|
|
1495
|
+
/** `true` only when the cached value is strictly `true`; `fallback` when the key is absent. */
|
|
1496
|
+
isEnabled(key, fallback = false) {
|
|
1497
|
+
return this.pool.isEnabled(key, fallback);
|
|
1498
|
+
}
|
|
1499
|
+
/** Alias of {@link isEnabled} (iOS parity). */
|
|
1500
|
+
bool(key, fallback = false) {
|
|
1501
|
+
return this.isEnabled(key, fallback);
|
|
1502
|
+
}
|
|
1503
|
+
/** Cached value when it is a string, else `fallback`. */
|
|
1504
|
+
getString(key, fallback) {
|
|
1505
|
+
const value = this.pool.all()[key];
|
|
1506
|
+
return typeof value === "string" ? value : fallback;
|
|
1507
|
+
}
|
|
1508
|
+
/** Cached value when it is an integer number, else `fallback`. */
|
|
1509
|
+
getInt(key, fallback) {
|
|
1510
|
+
const value = this.pool.all()[key];
|
|
1511
|
+
return typeof value === "number" && Number.isInteger(value) ? value : fallback;
|
|
1512
|
+
}
|
|
1513
|
+
/** Cached value when it is a number (integers included), else `fallback`. */
|
|
1514
|
+
getDouble(key, fallback) {
|
|
1515
|
+
const value = this.pool.all()[key];
|
|
1516
|
+
return typeof value === "number" ? value : fallback;
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Resolve the multivariate variant for `key` — the variant name, or `null`
|
|
1520
|
+
* when the flag has no variant (or the read fails).
|
|
1521
|
+
*
|
|
1522
|
+
* Wire failures (network errors, 404, etc.) resolve to `null`.
|
|
1523
|
+
* Invalid flag names throw `BackendError('validation', { code: 'invalid_flag_name' })`.
|
|
1524
|
+
*
|
|
1525
|
+
* DEVIATION from iOS sync-cache parity: the platform does not propagate
|
|
1526
|
+
* variant metadata into the user-flags snapshot/delta cache, so this is an
|
|
1527
|
+
* ASYNC transport read (`GET /v1/flags/{key}/variant`), not a cache lookup.
|
|
1528
|
+
*/
|
|
1529
|
+
async getVariant(key) {
|
|
1530
|
+
let res;
|
|
1531
|
+
try {
|
|
1532
|
+
res = await this.transport.getVariant(key);
|
|
1533
|
+
} catch (err) {
|
|
1534
|
+
throw new BackendError("validation", {
|
|
1535
|
+
code: "invalid_flag_name",
|
|
1536
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
return res.data?.name ?? null;
|
|
1540
|
+
}
|
|
1541
|
+
/** Subscribe to any change in the cached flag set. */
|
|
1542
|
+
onChange(callback) {
|
|
1543
|
+
return this.pool.onChange(callback);
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Observe ONE key: fires only when that key's value actually changes
|
|
1547
|
+
* (per {@link sameFlagValue} — structural compare for objects), with the
|
|
1548
|
+
* new value (`undefined` = deleted). P5 React-hook substrate.
|
|
1549
|
+
*/
|
|
1550
|
+
subscribeKey(key, callback) {
|
|
1551
|
+
let last = this.pool.all()[key];
|
|
1552
|
+
return this.pool.onChange(() => {
|
|
1553
|
+
const next = this.pool.all()[key];
|
|
1554
|
+
if (sameFlagValue(last, next)) return;
|
|
1555
|
+
last = next;
|
|
1556
|
+
callback(next);
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Async iteration over flag changes: yields the new {@link all} view on
|
|
1561
|
+
* every pool change notification. The listener is detached when the
|
|
1562
|
+
* consumer `break`s/`return`s/`throw`s — including while a `next()` is
|
|
1563
|
+
* still pending (it resolves `{ done: true }` instead of hanging, which a
|
|
1564
|
+
* plain async-generator `finally` would not guarantee).
|
|
1565
|
+
*/
|
|
1566
|
+
changes() {
|
|
1567
|
+
const queue = [];
|
|
1568
|
+
const pending = [];
|
|
1569
|
+
let finished = false;
|
|
1570
|
+
const unsubscribe = this.pool.onChange(() => {
|
|
1571
|
+
const snapshot = this.pool.all();
|
|
1572
|
+
const resolve = pending.shift();
|
|
1573
|
+
if (resolve) resolve({ value: snapshot, done: false });
|
|
1574
|
+
else queue.push(snapshot);
|
|
1575
|
+
});
|
|
1576
|
+
const finish = () => {
|
|
1577
|
+
if (finished) return;
|
|
1578
|
+
finished = true;
|
|
1579
|
+
unsubscribe();
|
|
1580
|
+
while (pending.length > 0) pending.shift()?.({ value: void 0, done: true });
|
|
1581
|
+
};
|
|
1582
|
+
return {
|
|
1583
|
+
next: () => {
|
|
1584
|
+
if (finished) return Promise.resolve({ value: void 0, done: true });
|
|
1585
|
+
const head = queue.shift();
|
|
1586
|
+
if (head !== void 0) return Promise.resolve({ value: head, done: false });
|
|
1587
|
+
return new Promise((resolve) => {
|
|
1588
|
+
pending.push(resolve);
|
|
1589
|
+
});
|
|
1590
|
+
},
|
|
1591
|
+
return: () => {
|
|
1592
|
+
finish();
|
|
1593
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
1594
|
+
},
|
|
1595
|
+
throw: (error) => {
|
|
1596
|
+
finish();
|
|
1597
|
+
return Promise.reject(error);
|
|
1598
|
+
},
|
|
1599
|
+
[Symbol.asyncIterator]() {
|
|
1600
|
+
return this;
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
/** Stop polling and detach all listeners (auth + visibility + subscribers). */
|
|
1605
|
+
destroy() {
|
|
1606
|
+
this.pool.destroy();
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
// src/messaging/util.ts
|
|
1611
|
+
function toBase64(bytes) {
|
|
1612
|
+
if (typeof Buffer !== "undefined") {
|
|
1613
|
+
return Buffer.from(bytes).toString("base64");
|
|
1614
|
+
}
|
|
1615
|
+
let binary = "";
|
|
1616
|
+
const chunk = 32768;
|
|
1617
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
1618
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
1619
|
+
}
|
|
1620
|
+
return btoa(binary);
|
|
1621
|
+
}
|
|
1622
|
+
function fromBase64(b64) {
|
|
1623
|
+
if (typeof Buffer !== "undefined") {
|
|
1624
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
1625
|
+
}
|
|
1626
|
+
const binary = atob(b64);
|
|
1627
|
+
const out = new Uint8Array(binary.length);
|
|
1628
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
1629
|
+
return out;
|
|
1630
|
+
}
|
|
1631
|
+
function bytesToHex(bytes) {
|
|
1632
|
+
let s = "";
|
|
1633
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1634
|
+
s += (bytes[i] ?? 0).toString(16).padStart(2, "0");
|
|
1635
|
+
}
|
|
1636
|
+
return s;
|
|
1637
|
+
}
|
|
1638
|
+
var utf8Encoder = new TextEncoder();
|
|
1639
|
+
var utf8Decoder = new TextDecoder();
|
|
1640
|
+
var encodeUtf8 = (s) => utf8Encoder.encode(s);
|
|
1641
|
+
var decodeUtf8 = (b) => utf8Decoder.decode(b);
|
|
1642
|
+
var deviceIdBytes = (deviceId) => encodeUtf8(deviceId);
|
|
1643
|
+
function randomId() {
|
|
1644
|
+
return crypto.randomUUID();
|
|
1645
|
+
}
|
|
1646
|
+
function mintDeviceId() {
|
|
1647
|
+
return `mdv_${uuidV7()}`;
|
|
1648
|
+
}
|
|
1649
|
+
function uuidV7() {
|
|
1650
|
+
const bytes = new Uint8Array(16);
|
|
1651
|
+
crypto.getRandomValues(bytes);
|
|
1652
|
+
const ts = Date.now();
|
|
1653
|
+
const view = new DataView(bytes.buffer);
|
|
1654
|
+
view.setUint32(0, Math.floor(ts / 2 ** 16));
|
|
1655
|
+
view.setUint16(4, ts & 65535);
|
|
1656
|
+
view.setUint8(6, view.getUint8(6) & 15 | 112);
|
|
1657
|
+
view.setUint8(8, view.getUint8(8) & 63 | 128);
|
|
1658
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1659
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
1660
|
+
}
|
|
1661
|
+
function directKey(userA, userB) {
|
|
1662
|
+
const [a, b] = [userA, userB].sort();
|
|
1663
|
+
return `dm:${a}|${b}`;
|
|
1664
|
+
}
|
|
1665
|
+
function mintClientMsgId() {
|
|
1666
|
+
const b = new Uint8Array(16);
|
|
1667
|
+
crypto.getRandomValues(b);
|
|
1668
|
+
return `msg_${Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("")}`;
|
|
1669
|
+
}
|
|
1670
|
+
function truncateQuote(s) {
|
|
1671
|
+
const scalars = [...s];
|
|
1672
|
+
if (scalars.length <= 240) return [s, false];
|
|
1673
|
+
return [scalars.slice(0, 240).join(""), true];
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// src/messaging/wire.ts
|
|
1677
|
+
var GROUPS = "/v1/messaging/groups";
|
|
1678
|
+
var DEVICES = "/v1/messaging/devices";
|
|
1679
|
+
var KEYDIR = "/v1/messaging/keydir";
|
|
1680
|
+
var seg = (s) => encodeURIComponent(s);
|
|
1681
|
+
var MessagingPaths = {
|
|
1682
|
+
groups: GROUPS,
|
|
1683
|
+
groupsDirect: `${GROUPS}/direct`,
|
|
1684
|
+
group: (displayId) => `${GROUPS}/${seg(displayId)}`,
|
|
1685
|
+
groupMetadata: (displayId) => `${GROUPS}/${seg(displayId)}/metadata`,
|
|
1686
|
+
groupMembers: (displayId) => `${GROUPS}/${seg(displayId)}/members`,
|
|
1687
|
+
groupMemberDevice: (displayId, deviceId) => `${GROUPS}/${seg(displayId)}/members/${seg(deviceId)}`,
|
|
1688
|
+
groupMessages: (displayId) => `${GROUPS}/${seg(displayId)}/messages`,
|
|
1689
|
+
groupCommits: (displayId) => `${GROUPS}/${seg(displayId)}/commits`,
|
|
1690
|
+
groupRead: (displayId) => `${GROUPS}/${seg(displayId)}/read`,
|
|
1691
|
+
deviceWelcomes: (deviceId) => `${DEVICES}/${seg(deviceId)}/welcomes`,
|
|
1692
|
+
deviceQueue: (deviceId) => `${DEVICES}/${seg(deviceId)}/queue`,
|
|
1693
|
+
deviceQueueAck: (deviceId) => `${DEVICES}/${seg(deviceId)}/queue/ack`,
|
|
1694
|
+
keydirDevices: `${KEYDIR}/devices`,
|
|
1695
|
+
keydirDeviceKeyPackages: (deviceId) => `${KEYDIR}/devices/${seg(deviceId)}/keypackages`,
|
|
1696
|
+
keydirUserDevices: (userId) => `${KEYDIR}/users/${seg(userId)}/devices`,
|
|
1697
|
+
keydirUserClaim: (userId) => `${KEYDIR}/users/${seg(userId)}/keypackages/claim`,
|
|
1698
|
+
keydirPinsVerify: `${KEYDIR}/pins/verify`,
|
|
1699
|
+
prefs: "/v1/messaging/prefs"
|
|
1700
|
+
};
|
|
1701
|
+
var ContentType = {
|
|
1702
|
+
application: 1,
|
|
1703
|
+
proposal: 2,
|
|
1704
|
+
commit: 3
|
|
1705
|
+
};
|
|
1706
|
+
|
|
1707
|
+
// src/messaging/keydir.ts
|
|
1708
|
+
async function enrollDevice(rt, engine, deviceStore, opts = {}) {
|
|
1709
|
+
const keyPackageCount = opts.keyPackageCount ?? 10;
|
|
1710
|
+
const deviceId = engine.deviceId;
|
|
1711
|
+
const alreadyEnrolled = await deviceStore.load(engine.userId) === deviceId;
|
|
1712
|
+
if (!alreadyEnrolled) {
|
|
1713
|
+
const material = await engine.enrollmentMaterial();
|
|
1714
|
+
const body = {
|
|
1715
|
+
device_id: deviceId,
|
|
1716
|
+
platform: "web",
|
|
1717
|
+
device_name: opts.deviceName,
|
|
1718
|
+
cipher_suite: 1,
|
|
1719
|
+
signature_key: toBase64(material.signatureKey),
|
|
1720
|
+
credential: toBase64(material.credential),
|
|
1721
|
+
capabilities: toBase64(material.capabilities)
|
|
1722
|
+
};
|
|
1723
|
+
await palbeRequest(rt, "POST", MessagingPaths.keydirDevices, { body });
|
|
1724
|
+
await deviceStore.save(engine.userId, deviceId);
|
|
1725
|
+
}
|
|
1726
|
+
if (keyPackageCount > 0) {
|
|
1727
|
+
const raws = [];
|
|
1728
|
+
for (let i = 0; i < keyPackageCount; i++) {
|
|
1729
|
+
raws.push(toBase64(await engine.generateKeyPackage()));
|
|
1730
|
+
}
|
|
1731
|
+
await replenish(rt, deviceId, raws);
|
|
1732
|
+
}
|
|
1733
|
+
return deviceId;
|
|
1734
|
+
}
|
|
1735
|
+
async function replenish(rt, deviceId, rawKeyPackagesB64) {
|
|
1736
|
+
const body = { key_packages: rawKeyPackagesB64 };
|
|
1737
|
+
await palbeRequest(rt, "POST", MessagingPaths.keydirDeviceKeyPackages(deviceId), { body });
|
|
1738
|
+
}
|
|
1739
|
+
async function claimKeyPackages(rt, userId) {
|
|
1740
|
+
return palbeRequest(rt, "POST", MessagingPaths.keydirUserClaim(userId), {
|
|
1741
|
+
body: {}
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
async function listDevices(rt, userId) {
|
|
1745
|
+
return palbeRequest(rt, "GET", MessagingPaths.keydirUserDevices(userId));
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// src/messaging/group-messaging.ts
|
|
1749
|
+
function encodeEnvelope(args) {
|
|
1750
|
+
const env = {
|
|
1751
|
+
v: 1,
|
|
1752
|
+
type: "text",
|
|
1753
|
+
client_msg_id: args.clientMsgId,
|
|
1754
|
+
text: args.text,
|
|
1755
|
+
...args.replyTo ? { reply_to: args.replyTo } : {}
|
|
1756
|
+
};
|
|
1757
|
+
return encodeUtf8(JSON.stringify(env));
|
|
1758
|
+
}
|
|
1759
|
+
function decodeEnvelope(bytes) {
|
|
1760
|
+
const s = decodeUtf8(bytes);
|
|
1761
|
+
try {
|
|
1762
|
+
const o = JSON.parse(s);
|
|
1763
|
+
if (typeof o === "object" && o !== null && o.type === "text" && typeof o.v === "number") {
|
|
1764
|
+
return {
|
|
1765
|
+
text: o.text ?? null,
|
|
1766
|
+
clientMsgId: o.client_msg_id ?? "",
|
|
1767
|
+
replyTo: o.reply_to ?? null
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
if (typeof o === "object" && o !== null && o.type === "text") {
|
|
1771
|
+
return { text: o.text ?? null, clientMsgId: "", replyTo: null };
|
|
1772
|
+
}
|
|
1773
|
+
if (typeof o === "object" && o !== null && o.type === "media")
|
|
1774
|
+
return { text: null, clientMsgId: "", replyTo: null };
|
|
1775
|
+
} catch {
|
|
1776
|
+
}
|
|
1777
|
+
return { text: s, clientMsgId: "", replyTo: null };
|
|
1778
|
+
}
|
|
1779
|
+
function resolveReply(ref, lookup) {
|
|
1780
|
+
const parent = lookup(ref.client_msg_id);
|
|
1781
|
+
if (parent !== null) {
|
|
1782
|
+
return {
|
|
1783
|
+
parentClientMsgId: ref.client_msg_id,
|
|
1784
|
+
state: "resolved",
|
|
1785
|
+
quoteSenderUserId: parent.senderUserId,
|
|
1786
|
+
quoteText: parent.text,
|
|
1787
|
+
quoteKind: ref.preview?.kind ?? "text"
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
return {
|
|
1791
|
+
parentClientMsgId: ref.client_msg_id,
|
|
1792
|
+
state: "unavailable",
|
|
1793
|
+
quoteSenderUserId: ref.preview?.author_user_id ?? "",
|
|
1794
|
+
quoteText: ref.preview?.body ?? null,
|
|
1795
|
+
quoteKind: ref.preview?.kind ?? "text"
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
var DEFAULT_REBASE = { maxAttempts: 6, baseDelayMs: 80, jitterMs: 120 };
|
|
1799
|
+
var RebaseExhausted = class extends Error {
|
|
1800
|
+
constructor(attempts, lastKnownEpoch) {
|
|
1801
|
+
super(`commit rebase exhausted after ${attempts} attempts`);
|
|
1802
|
+
this.attempts = attempts;
|
|
1803
|
+
this.lastKnownEpoch = lastKnownEpoch;
|
|
1804
|
+
this.name = "RebaseExhausted";
|
|
1805
|
+
}
|
|
1806
|
+
attempts;
|
|
1807
|
+
lastKnownEpoch;
|
|
1808
|
+
};
|
|
1809
|
+
var GroupMessaging = class {
|
|
1810
|
+
constructor(rt, engine, selfUserId, selfDeviceId, messageStore, rebase = DEFAULT_REBASE) {
|
|
1811
|
+
this.rt = rt;
|
|
1812
|
+
this.engine = engine;
|
|
1813
|
+
this.selfUserId = selfUserId;
|
|
1814
|
+
this.selfDeviceId = selfDeviceId;
|
|
1815
|
+
this.messageStore = messageStore;
|
|
1816
|
+
this.rebase = rebase;
|
|
1817
|
+
}
|
|
1818
|
+
rt;
|
|
1819
|
+
engine;
|
|
1820
|
+
selfUserId;
|
|
1821
|
+
selfDeviceId;
|
|
1822
|
+
messageStore;
|
|
1823
|
+
rebase;
|
|
1824
|
+
// ── Create ──
|
|
1825
|
+
/** Create a group: mint the rfc_group_id, POST create, return the server group.
|
|
1826
|
+
* No name sealing on web (metadata omitted — server-blind, label fallback). */
|
|
1827
|
+
async createGroup(cipherSuite = 1) {
|
|
1828
|
+
const rfcGroupId = await this.engine.createGroup();
|
|
1829
|
+
const body = {
|
|
1830
|
+
rfc_group_id: toBase64(rfcGroupId),
|
|
1831
|
+
cipher_suite: cipherSuite,
|
|
1832
|
+
creator_device_id: this.selfDeviceId,
|
|
1833
|
+
creator_leaf_index: 0
|
|
1834
|
+
};
|
|
1835
|
+
const res = await palbeRequest(this.rt, "POST", MessagingPaths.groups, {
|
|
1836
|
+
body
|
|
1837
|
+
});
|
|
1838
|
+
return {
|
|
1839
|
+
displayId: res.display_id,
|
|
1840
|
+
rfcGroupId: res.rfc_group_id,
|
|
1841
|
+
currentEpoch: res.current_epoch,
|
|
1842
|
+
ownerUserId: this.selfUserId,
|
|
1843
|
+
directKey: null,
|
|
1844
|
+
name: null
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
// ── Direct (1:1) get-or-create ──
|
|
1848
|
+
/** Materialize a DIRECT chat keyed by the stable direct_key (sorted user pair).
|
|
1849
|
+
* EXISTS → adopt (the Welcome drains via the pump); ABSENT → create + add peer. */
|
|
1850
|
+
async getOrCreateDirect(peerUserId) {
|
|
1851
|
+
const dk = directKey(this.selfUserId, peerUserId);
|
|
1852
|
+
const rfcGroupId = await this.engine.createGroup();
|
|
1853
|
+
const body = {
|
|
1854
|
+
direct_key: dk,
|
|
1855
|
+
peer_user_id: peerUserId,
|
|
1856
|
+
rfc_group_id: toBase64(rfcGroupId),
|
|
1857
|
+
cipher_suite: 1,
|
|
1858
|
+
creator_device_id: this.selfDeviceId,
|
|
1859
|
+
creator_leaf_index: 0
|
|
1860
|
+
};
|
|
1861
|
+
const res = await palbeRequest(
|
|
1862
|
+
this.rt,
|
|
1863
|
+
"POST",
|
|
1864
|
+
MessagingPaths.groupsDirect,
|
|
1865
|
+
{ body }
|
|
1866
|
+
);
|
|
1867
|
+
if (res.exists === true) {
|
|
1868
|
+
await this.engine.clearPendingCommit(rfcGroupId);
|
|
1869
|
+
return {
|
|
1870
|
+
displayId: res.display_id,
|
|
1871
|
+
rfcGroupId: res.rfc_group_id,
|
|
1872
|
+
currentEpoch: res.current_epoch,
|
|
1873
|
+
ownerUserId: res.owner_user_id ?? null,
|
|
1874
|
+
directKey: dk,
|
|
1875
|
+
name: null
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
const group = {
|
|
1879
|
+
displayId: res.display_id,
|
|
1880
|
+
rfcGroupId: res.rfc_group_id,
|
|
1881
|
+
currentEpoch: res.current_epoch,
|
|
1882
|
+
ownerUserId: this.selfUserId,
|
|
1883
|
+
directKey: dk,
|
|
1884
|
+
name: null
|
|
1885
|
+
};
|
|
1886
|
+
const claimed = await claimKeyPackages(this.rt, peerUserId);
|
|
1887
|
+
await this.addMember(group, peerUserId, claimed);
|
|
1888
|
+
return group;
|
|
1889
|
+
}
|
|
1890
|
+
// ── Members ──
|
|
1891
|
+
/** The group roster (device leaves). The coordinator dedups to users. */
|
|
1892
|
+
async listMembers(displayId) {
|
|
1893
|
+
const res = await palbeRequest(
|
|
1894
|
+
this.rt,
|
|
1895
|
+
"GET",
|
|
1896
|
+
MessagingPaths.groupMembers(displayId)
|
|
1897
|
+
);
|
|
1898
|
+
return res.members;
|
|
1899
|
+
}
|
|
1900
|
+
/** Add a member: claim → stage the add commit → run the rebase loop. SP-1.6:
|
|
1901
|
+
* EVERY device of the user joins in ONE atomic commit (one shared Welcome). */
|
|
1902
|
+
async addMember(group, userId, claimed) {
|
|
1903
|
+
const claim = claimed ?? await claimKeyPackages(this.rt, userId);
|
|
1904
|
+
const adds = claim.key_packages.map((kp) => {
|
|
1905
|
+
try {
|
|
1906
|
+
return { deviceId: kp.device_id, kp: fromBase64(kp.key_package) };
|
|
1907
|
+
} catch {
|
|
1908
|
+
return null;
|
|
1909
|
+
}
|
|
1910
|
+
}).filter((x) => x !== null);
|
|
1911
|
+
if (adds.length === 0) {
|
|
1912
|
+
throw new BackendError("validation", {
|
|
1913
|
+
code: "no_keypackage",
|
|
1914
|
+
message: "no claimable key package for user",
|
|
1915
|
+
status: 422
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
const userIds = adds.map(() => userId);
|
|
1919
|
+
await this.commitWithRebase(group.rfcGroupId, async () => {
|
|
1920
|
+
const out = await this.engine.commitChanges(
|
|
1921
|
+
fromBase64(group.rfcGroupId),
|
|
1922
|
+
adds.map((a) => a.kp),
|
|
1923
|
+
[]
|
|
1924
|
+
);
|
|
1925
|
+
const welcomeB64 = out.welcome ? toBase64(out.welcome) : "";
|
|
1926
|
+
const welcomes = adds.map((a) => ({
|
|
1927
|
+
recipient_device_id: a.deviceId,
|
|
1928
|
+
recipient_user_id: userId,
|
|
1929
|
+
welcome_b64: welcomeB64
|
|
1930
|
+
}));
|
|
1931
|
+
const memberBody = {
|
|
1932
|
+
op: "add",
|
|
1933
|
+
commit_b64: toBase64(out.commit),
|
|
1934
|
+
target_device_ids: adds.map((a) => a.deviceId),
|
|
1935
|
+
leaf_indexes: out.addedLeafIndices,
|
|
1936
|
+
user_ids: userIds,
|
|
1937
|
+
key_package_refs: [],
|
|
1938
|
+
welcomes
|
|
1939
|
+
};
|
|
1940
|
+
return {
|
|
1941
|
+
method: "POST",
|
|
1942
|
+
path: MessagingPaths.groupMembers(group.displayId),
|
|
1943
|
+
body: memberBody
|
|
1944
|
+
};
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
/** Remove a user = evict ALL their device leaves in ONE commit (SP-1.6 D4). */
|
|
1948
|
+
async removeMember(group, userId) {
|
|
1949
|
+
const members = await this.listMembers(group.displayId);
|
|
1950
|
+
const deviceIds = members.filter((m) => m.user_id === userId).map((m) => m.device_id);
|
|
1951
|
+
if (deviceIds.length === 0) {
|
|
1952
|
+
throw new BackendError("validation", {
|
|
1953
|
+
code: "not_a_member",
|
|
1954
|
+
message: "user has no device in this group",
|
|
1955
|
+
status: 404
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
await this.stageMultiRemove(group, deviceIds, [userId]);
|
|
1959
|
+
}
|
|
1960
|
+
/** Leave = remove the CALLING device's leaf only (per-device self-leave). */
|
|
1961
|
+
async leaveGroup(group) {
|
|
1962
|
+
await this.stageMultiRemove(group, [this.selfDeviceId], [this.selfUserId]);
|
|
1963
|
+
}
|
|
1964
|
+
async stageMultiRemove(group, deviceIds, removedUserIds) {
|
|
1965
|
+
const pathDeviceId = deviceIds[0];
|
|
1966
|
+
if (!pathDeviceId) throw new Error("no device to remove");
|
|
1967
|
+
await this.commitWithRebase(group.rfcGroupId, async () => {
|
|
1968
|
+
const out = await this.engine.commitChanges(
|
|
1969
|
+
fromBase64(group.rfcGroupId),
|
|
1970
|
+
[],
|
|
1971
|
+
deviceIds.map(deviceIdBytes)
|
|
1972
|
+
);
|
|
1973
|
+
const body = {
|
|
1974
|
+
op: "remove",
|
|
1975
|
+
commit_b64: toBase64(out.commit),
|
|
1976
|
+
target_device_ids: deviceIds,
|
|
1977
|
+
user_ids: removedUserIds
|
|
1978
|
+
};
|
|
1979
|
+
return {
|
|
1980
|
+
method: "DELETE",
|
|
1981
|
+
path: MessagingPaths.groupMemberDevice(group.displayId, pathDeviceId),
|
|
1982
|
+
body
|
|
1983
|
+
};
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
// ── Send ──
|
|
1987
|
+
/** Send a text message. Encrypt at the current epoch, POST, return the receipt +
|
|
1988
|
+
* the client-minted `clientMsgId` (needed by the Chat layer for reply indexing).
|
|
1989
|
+
* NEVER rebases (a 422 stale_application surfaces to the caller). */
|
|
1990
|
+
async sendText(group, text, replyTo) {
|
|
1991
|
+
const clientMsgId = mintClientMsgId();
|
|
1992
|
+
const plaintext = encodeEnvelope({ text, clientMsgId, replyTo });
|
|
1993
|
+
const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
|
|
1994
|
+
const body = {
|
|
1995
|
+
ciphertext_b64: toBase64(ct),
|
|
1996
|
+
client_idem_key: randomId()
|
|
1997
|
+
};
|
|
1998
|
+
const wire = await palbeRequest(
|
|
1999
|
+
this.rt,
|
|
2000
|
+
"POST",
|
|
2001
|
+
MessagingPaths.groupMessages(group.displayId),
|
|
2002
|
+
{ body }
|
|
2003
|
+
);
|
|
2004
|
+
const stored = {
|
|
2005
|
+
id: `${group.rfcGroupId}#${wire.server_seq}`,
|
|
2006
|
+
direction: "outgoing",
|
|
2007
|
+
text,
|
|
2008
|
+
senderDeviceId: this.selfDeviceId,
|
|
2009
|
+
epoch: wire.epoch,
|
|
2010
|
+
serverSeq: wire.server_seq,
|
|
2011
|
+
at: Date.now(),
|
|
2012
|
+
clientMsgId,
|
|
2013
|
+
replyTo: replyTo ? {
|
|
2014
|
+
clientMsgId: replyTo.client_msg_id,
|
|
2015
|
+
previewBody: replyTo.preview?.body ?? null,
|
|
2016
|
+
previewAuthorUserId: replyTo.preview?.author_user_id ?? null,
|
|
2017
|
+
previewKind: replyTo.preview?.kind ?? "text"
|
|
2018
|
+
} : null
|
|
2019
|
+
};
|
|
2020
|
+
try {
|
|
2021
|
+
await this.messageStore.append(group.rfcGroupId, stored);
|
|
2022
|
+
} catch {
|
|
2023
|
+
}
|
|
2024
|
+
return { receipt: { serverSeq: wire.server_seq, epoch: wire.epoch }, clientMsgId };
|
|
2025
|
+
}
|
|
2026
|
+
// ── The rebase loop ──
|
|
2027
|
+
async commitWithRebase(rfcGroupId, build) {
|
|
2028
|
+
const gidBytes = fromBase64(rfcGroupId);
|
|
2029
|
+
let lastEpoch = null;
|
|
2030
|
+
for (let attempt = 1; attempt <= this.rebase.maxAttempts; attempt++) {
|
|
2031
|
+
const pending = await build();
|
|
2032
|
+
try {
|
|
2033
|
+
await palbeRequest(this.rt, pending.method, pending.path, {
|
|
2034
|
+
body: pending.body
|
|
2035
|
+
});
|
|
2036
|
+
await this.engine.applyPendingCommit(gidBytes);
|
|
2037
|
+
return;
|
|
2038
|
+
} catch (e) {
|
|
2039
|
+
if (isBackendError(e) && e.code === "epoch_conflict") {
|
|
2040
|
+
await this.engine.clearPendingCommit(gidBytes);
|
|
2041
|
+
const ce = e.data?.current_epoch;
|
|
2042
|
+
if (typeof ce === "number") lastEpoch = ce;
|
|
2043
|
+
if (attempt === this.rebase.maxAttempts) break;
|
|
2044
|
+
await this.drain(rfcGroupId);
|
|
2045
|
+
await this.backoff(attempt);
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
throw e;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
await this.engine.clearPendingCommit(gidBytes);
|
|
2052
|
+
throw new RebaseExhausted(this.rebase.maxAttempts, lastEpoch);
|
|
2053
|
+
}
|
|
2054
|
+
/** Pull the device queue, process the winning commit(s) so the engine advances
|
|
2055
|
+
* to the new epoch, ack the commits (the delivery source owns app messages). */
|
|
2056
|
+
async drain(rfcGroupId) {
|
|
2057
|
+
const page = await palbeRequest(
|
|
2058
|
+
this.rt,
|
|
2059
|
+
"GET",
|
|
2060
|
+
MessagingPaths.deviceQueue(this.selfDeviceId)
|
|
2061
|
+
);
|
|
2062
|
+
const gidBytes = fromBase64(rfcGroupId);
|
|
2063
|
+
const ackIds = [];
|
|
2064
|
+
for (const row of page.messages) {
|
|
2065
|
+
if (row.content_type !== ContentType.commit) continue;
|
|
2066
|
+
try {
|
|
2067
|
+
await this.engine.processIncoming(gidBytes, fromBase64(row.ciphertext_b64));
|
|
2068
|
+
} catch {
|
|
2069
|
+
}
|
|
2070
|
+
ackIds.push(row.id);
|
|
2071
|
+
}
|
|
2072
|
+
if (ackIds.length > 0) {
|
|
2073
|
+
await palbeRequest(this.rt, "POST", MessagingPaths.deviceQueueAck(this.selfDeviceId), {
|
|
2074
|
+
body: { message_ids: ackIds }
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
async backoff(attempt) {
|
|
2079
|
+
const base = this.rebase.baseDelayMs * attempt;
|
|
2080
|
+
const jitter = this.rebase.jitterMs === 0 ? 0 : Math.random() * this.rebase.jitterMs;
|
|
2081
|
+
const total = base + jitter;
|
|
2082
|
+
if (total <= 0) return;
|
|
2083
|
+
await new Promise((r) => setTimeout(r, total));
|
|
2084
|
+
}
|
|
2085
|
+
};
|
|
2086
|
+
|
|
2087
|
+
// src/messaging/chat.ts
|
|
2088
|
+
var Chat = class {
|
|
2089
|
+
/** Stable, URL/log-safe id (the grp_ display id once active; a reserved local id while draft). */
|
|
2090
|
+
id;
|
|
2091
|
+
kind;
|
|
2092
|
+
_state;
|
|
2093
|
+
_group;
|
|
2094
|
+
_draft;
|
|
2095
|
+
backend;
|
|
2096
|
+
messageList = [];
|
|
2097
|
+
memberCache = [];
|
|
2098
|
+
typingList = [];
|
|
2099
|
+
presenceStates = /* @__PURE__ */ new Map();
|
|
2100
|
+
readWatermark = -1;
|
|
2101
|
+
titleOverride = null;
|
|
2102
|
+
seenKeys = /* @__PURE__ */ new Set();
|
|
2103
|
+
/** Index: clientMsgId → { text, senderUserId } for resolveReply lookups. */
|
|
2104
|
+
byClientMsgId = /* @__PURE__ */ new Map();
|
|
2105
|
+
loadedEarliestSeq = null;
|
|
2106
|
+
historyLoaded = false;
|
|
2107
|
+
wired = false;
|
|
2108
|
+
liveUnsub = null;
|
|
2109
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2110
|
+
/** @internal — obtain via pb.messaging.directChat / groupChat / chat(id). */
|
|
2111
|
+
constructor(args) {
|
|
2112
|
+
this.backend = args.backend;
|
|
2113
|
+
if ("group" in args) {
|
|
2114
|
+
this.id = args.group.displayId;
|
|
2115
|
+
this.kind = args.kind;
|
|
2116
|
+
this._state = "active";
|
|
2117
|
+
this._group = args.group;
|
|
2118
|
+
this._draft = null;
|
|
2119
|
+
this.seedMembersFromGroup(args.group);
|
|
2120
|
+
} else {
|
|
2121
|
+
this.id = args.draft.reservedId;
|
|
2122
|
+
this.kind = args.kind;
|
|
2123
|
+
this._state = "draft";
|
|
2124
|
+
this._group = null;
|
|
2125
|
+
this._draft = args.draft;
|
|
2126
|
+
this.seedDraftMembers(args.draft);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
// ── Observability ──
|
|
2130
|
+
/** Subscribe to any change in this chat (messages/members/typing/state). */
|
|
2131
|
+
onChange(cb) {
|
|
2132
|
+
this.listeners.add(cb);
|
|
2133
|
+
this.ensureWired();
|
|
2134
|
+
return () => this.listeners.delete(cb);
|
|
2135
|
+
}
|
|
2136
|
+
emit() {
|
|
2137
|
+
for (const cb of this.listeners) cb();
|
|
2138
|
+
}
|
|
2139
|
+
// ── Snapshot getters ──
|
|
2140
|
+
get state() {
|
|
2141
|
+
return this._state;
|
|
2142
|
+
}
|
|
2143
|
+
get isDirect() {
|
|
2144
|
+
return this.kind === "direct";
|
|
2145
|
+
}
|
|
2146
|
+
get messages() {
|
|
2147
|
+
return this.messageList;
|
|
2148
|
+
}
|
|
2149
|
+
get members() {
|
|
2150
|
+
return this.memberCache;
|
|
2151
|
+
}
|
|
2152
|
+
get typing() {
|
|
2153
|
+
return this.typingList;
|
|
2154
|
+
}
|
|
2155
|
+
get lastMessage() {
|
|
2156
|
+
return this.messageList.at(-1) ?? null;
|
|
2157
|
+
}
|
|
2158
|
+
get unreadCount() {
|
|
2159
|
+
return this.messageList.filter(
|
|
2160
|
+
(m) => m.direction === "incoming" && m.serverSeq > this.readWatermark
|
|
2161
|
+
).length;
|
|
2162
|
+
}
|
|
2163
|
+
get title() {
|
|
2164
|
+
if (this.titleOverride) return this.titleOverride;
|
|
2165
|
+
if (this._group?.name) return this._group.name;
|
|
2166
|
+
if (this._draft) {
|
|
2167
|
+
const mode = this._draft.mode;
|
|
2168
|
+
if (mode.kind === "direct") {
|
|
2169
|
+
const peer = this.memberCache.find((m) => m.userId === mode.peerUserId);
|
|
2170
|
+
return peer?.displayName ?? "Chat";
|
|
2171
|
+
}
|
|
2172
|
+
return "Group";
|
|
2173
|
+
}
|
|
2174
|
+
const tail = this.id.startsWith("grp_") ? this.id.slice(4) : this.id;
|
|
2175
|
+
const short = tail.slice(-4);
|
|
2176
|
+
return short ? `Group ${short}` : "Group";
|
|
2177
|
+
}
|
|
2178
|
+
presence(userId) {
|
|
2179
|
+
return this.presenceStates.get(userId) ?? null;
|
|
2180
|
+
}
|
|
2181
|
+
// ── Wiring (live + history) ──
|
|
2182
|
+
ensureWired() {
|
|
2183
|
+
if (this.wired || this._state !== "active" || !this._group) return;
|
|
2184
|
+
this.wired = true;
|
|
2185
|
+
this.liveUnsub = this.backend.subscribeLive(this._group, this);
|
|
2186
|
+
void this.hydrateHistory();
|
|
2187
|
+
void this.refreshMembers();
|
|
2188
|
+
}
|
|
2189
|
+
async hydrateHistory() {
|
|
2190
|
+
if (this.historyLoaded || !this._group) return;
|
|
2191
|
+
this.historyLoaded = true;
|
|
2192
|
+
const entries = await this.backend.history(this._group, 50);
|
|
2193
|
+
this.mergeHistory(entries);
|
|
2194
|
+
}
|
|
2195
|
+
mergeHistory(incoming) {
|
|
2196
|
+
let changed = false;
|
|
2197
|
+
for (const m of incoming) {
|
|
2198
|
+
if (m.serverSeq <= 0) continue;
|
|
2199
|
+
if (m.clientMsgId && m.text !== null) {
|
|
2200
|
+
this.byClientMsgId.set(m.clientMsgId, {
|
|
2201
|
+
text: m.text,
|
|
2202
|
+
senderUserId: m.senderUserId ?? ""
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
for (const m of incoming) {
|
|
2207
|
+
if (m.serverSeq <= 0) continue;
|
|
2208
|
+
const key = this.internalKey(m.serverSeq);
|
|
2209
|
+
if (this.seenKeys.has(key)) continue;
|
|
2210
|
+
this.seenKeys.add(key);
|
|
2211
|
+
this.messageList.push(m);
|
|
2212
|
+
changed = true;
|
|
2213
|
+
this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
|
|
2214
|
+
}
|
|
2215
|
+
if (changed) {
|
|
2216
|
+
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
2217
|
+
this.emit();
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
/** @internal — called by the backend's live subscription. */
|
|
2221
|
+
async ingestLive(incoming) {
|
|
2222
|
+
if (!this._group) return;
|
|
2223
|
+
if (incoming.kind === "commit") {
|
|
2224
|
+
await this.refreshMembers();
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
if (incoming.serverSeq <= 0) return;
|
|
2228
|
+
const key = this.internalKey(incoming.serverSeq);
|
|
2229
|
+
if (this.seenKeys.has(key)) return;
|
|
2230
|
+
this.seenKeys.add(key);
|
|
2231
|
+
let senderUser = null;
|
|
2232
|
+
if (incoming.senderDeviceId) {
|
|
2233
|
+
senderUser = await this.backend.userIdForDevice(this._group, incoming.senderDeviceId);
|
|
2234
|
+
}
|
|
2235
|
+
const direction = senderUser !== null && senderUser === this.backend.selfUserId ? "outgoing" : "incoming";
|
|
2236
|
+
const incomingClientMsgId = incoming.clientMsgId;
|
|
2237
|
+
const incomingReplyRef = incoming.replyRef;
|
|
2238
|
+
let resolvedReplyTo = null;
|
|
2239
|
+
if (incomingReplyRef) {
|
|
2240
|
+
resolvedReplyTo = resolveReply(incomingReplyRef, (id) => this.byClientMsgId.get(id) ?? null);
|
|
2241
|
+
}
|
|
2242
|
+
const msg = {
|
|
2243
|
+
id: this.publicId(incoming.serverSeq),
|
|
2244
|
+
kind: this.kindOf(incoming),
|
|
2245
|
+
direction,
|
|
2246
|
+
senderUserId: senderUser,
|
|
2247
|
+
text: incoming.text,
|
|
2248
|
+
serverSeq: incoming.serverSeq,
|
|
2249
|
+
sentAt: incoming.receivedAt,
|
|
2250
|
+
clientMsgId: incomingClientMsgId,
|
|
2251
|
+
replyTo: resolvedReplyTo
|
|
2252
|
+
};
|
|
2253
|
+
if (incomingClientMsgId && incoming.text !== null) {
|
|
2254
|
+
this.byClientMsgId.set(incomingClientMsgId, {
|
|
2255
|
+
text: incoming.text,
|
|
2256
|
+
senderUserId: senderUser ?? ""
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
this.messageList.push(msg);
|
|
2260
|
+
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
2261
|
+
this.loadedEarliestSeq = Math.min(
|
|
2262
|
+
this.loadedEarliestSeq ?? incoming.serverSeq,
|
|
2263
|
+
incoming.serverSeq
|
|
2264
|
+
);
|
|
2265
|
+
this.emit();
|
|
2266
|
+
}
|
|
2267
|
+
/** @internal — called by the backend's conv subscription. */
|
|
2268
|
+
applyConv(event, payload) {
|
|
2269
|
+
const userId = typeof payload.user_id === "string" ? payload.user_id : null;
|
|
2270
|
+
if (!userId || userId === this.backend.selfUserId) return;
|
|
2271
|
+
if (event === "typing") {
|
|
2272
|
+
const isTyping = payload.typing === true;
|
|
2273
|
+
const member = this.memberCache.find((m) => m.userId === userId) ?? {
|
|
2274
|
+
id: userId,
|
|
2275
|
+
userId,
|
|
2276
|
+
displayName: null,
|
|
2277
|
+
role: "member",
|
|
2278
|
+
isSelf: false
|
|
2279
|
+
};
|
|
2280
|
+
this.typingList = isTyping ? [...this.typingList.filter((m) => m.userId !== userId), member] : this.typingList.filter((m) => m.userId !== userId);
|
|
2281
|
+
this.emit();
|
|
2282
|
+
} else if (event === "presence") {
|
|
2283
|
+
const online = payload.online === true;
|
|
2284
|
+
const ts = typeof payload.ts === "number" ? payload.ts : null;
|
|
2285
|
+
this.presenceStates.set(userId, {
|
|
2286
|
+
userId,
|
|
2287
|
+
isOnline: online,
|
|
2288
|
+
lastSeenAt: ts ? new Date(ts * 1e3) : null
|
|
2289
|
+
});
|
|
2290
|
+
this.emit();
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
kindOf(incoming) {
|
|
2294
|
+
if (incoming.kind === "commit" || incoming.kind === "proposal" || incoming.kind === "welcome")
|
|
2295
|
+
return "system";
|
|
2296
|
+
return "text";
|
|
2297
|
+
}
|
|
2298
|
+
internalKey(serverSeq) {
|
|
2299
|
+
return this._group ? `${this._group.rfcGroupId}#${serverSeq}` : `draft#${serverSeq}`;
|
|
2300
|
+
}
|
|
2301
|
+
publicId(serverSeq) {
|
|
2302
|
+
return `${this.id}#${serverSeq}`;
|
|
2303
|
+
}
|
|
2304
|
+
/** Page older messages in. Returns how many were prepended. */
|
|
2305
|
+
async loadEarlier(limit = 50) {
|
|
2306
|
+
if (!this._group) return 0;
|
|
2307
|
+
const before = this.loadedEarliestSeq ?? void 0;
|
|
2308
|
+
const older = await this.backend.history(this._group, limit, before);
|
|
2309
|
+
const before0 = this.messageList.length;
|
|
2310
|
+
this.mergeHistory(older);
|
|
2311
|
+
return this.messageList.length - before0;
|
|
2312
|
+
}
|
|
2313
|
+
// ── Members ──
|
|
2314
|
+
async refreshMembers() {
|
|
2315
|
+
if (!this._group) return;
|
|
2316
|
+
const m = await this.backend.members(this._group);
|
|
2317
|
+
if (m.length > 0) {
|
|
2318
|
+
this.memberCache = m;
|
|
2319
|
+
this.emit();
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
seedMembersFromGroup(group) {
|
|
2323
|
+
const seed = [
|
|
2324
|
+
{
|
|
2325
|
+
id: this.backend.selfUserId,
|
|
2326
|
+
userId: this.backend.selfUserId,
|
|
2327
|
+
displayName: null,
|
|
2328
|
+
role: "owner",
|
|
2329
|
+
isSelf: true
|
|
2330
|
+
}
|
|
2331
|
+
];
|
|
2332
|
+
if (group.ownerUserId && group.ownerUserId !== this.backend.selfUserId) {
|
|
2333
|
+
seed.push({
|
|
2334
|
+
id: group.ownerUserId,
|
|
2335
|
+
userId: group.ownerUserId,
|
|
2336
|
+
displayName: null,
|
|
2337
|
+
role: "owner",
|
|
2338
|
+
isSelf: false
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
this.memberCache = seed;
|
|
2342
|
+
}
|
|
2343
|
+
seedDraftMembers(draft) {
|
|
2344
|
+
const seed = [
|
|
2345
|
+
{
|
|
2346
|
+
id: this.backend.selfUserId,
|
|
2347
|
+
userId: this.backend.selfUserId,
|
|
2348
|
+
displayName: null,
|
|
2349
|
+
role: draft.mode.kind === "group" ? "owner" : "member",
|
|
2350
|
+
isSelf: true
|
|
2351
|
+
}
|
|
2352
|
+
];
|
|
2353
|
+
if (draft.mode.kind === "direct") {
|
|
2354
|
+
seed.push({
|
|
2355
|
+
id: draft.mode.peerUserId,
|
|
2356
|
+
userId: draft.mode.peerUserId,
|
|
2357
|
+
displayName: null,
|
|
2358
|
+
role: "member",
|
|
2359
|
+
isSelf: false
|
|
2360
|
+
});
|
|
2361
|
+
} else {
|
|
2362
|
+
for (const uid of draft.mode.members) {
|
|
2363
|
+
seed.push({ id: uid, userId: uid, displayName: null, role: "member", isSelf: false });
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
this.memberCache = seed;
|
|
2367
|
+
}
|
|
2368
|
+
// ── Send (with WhatsApp lazy materialize) ──
|
|
2369
|
+
async send(text, opts) {
|
|
2370
|
+
const group = await this.materializeIfNeeded();
|
|
2371
|
+
let replyRef = null;
|
|
2372
|
+
let resolvedReplyTo = null;
|
|
2373
|
+
const parent = opts?.replyTo;
|
|
2374
|
+
if (parent?.clientMsgId && parent.kind !== "system") {
|
|
2375
|
+
const [previewBody, wasTruncated] = parent.text ? truncateQuote(parent.text) : ["", false];
|
|
2376
|
+
replyRef = {
|
|
2377
|
+
v: 1,
|
|
2378
|
+
client_msg_id: parent.clientMsgId,
|
|
2379
|
+
preview: {
|
|
2380
|
+
kind: "text",
|
|
2381
|
+
author_user_id: parent.senderUserId ?? "",
|
|
2382
|
+
body: previewBody || void 0,
|
|
2383
|
+
body_truncated: wasTruncated
|
|
2384
|
+
}
|
|
2385
|
+
};
|
|
2386
|
+
resolvedReplyTo = resolveReply(replyRef, (id) => this.byClientMsgId.get(id) ?? null);
|
|
2387
|
+
}
|
|
2388
|
+
const { receipt, clientMsgId } = await this.backend.sendText(group, text, replyRef);
|
|
2389
|
+
this.appendOwnSend(text, receipt, clientMsgId, resolvedReplyTo);
|
|
2390
|
+
return receipt;
|
|
2391
|
+
}
|
|
2392
|
+
appendOwnSend(text, receipt, clientMsgId, resolvedReplyTo) {
|
|
2393
|
+
if (receipt.serverSeq <= 0) return;
|
|
2394
|
+
const key = this.internalKey(receipt.serverSeq);
|
|
2395
|
+
if (this.seenKeys.has(key)) return;
|
|
2396
|
+
this.seenKeys.add(key);
|
|
2397
|
+
if (clientMsgId) {
|
|
2398
|
+
this.byClientMsgId.set(clientMsgId, { text, senderUserId: this.backend.selfUserId });
|
|
2399
|
+
}
|
|
2400
|
+
this.messageList.push({
|
|
2401
|
+
id: this.publicId(receipt.serverSeq),
|
|
2402
|
+
kind: "text",
|
|
2403
|
+
direction: "outgoing",
|
|
2404
|
+
senderUserId: this.backend.selfUserId,
|
|
2405
|
+
text,
|
|
2406
|
+
serverSeq: receipt.serverSeq,
|
|
2407
|
+
sentAt: /* @__PURE__ */ new Date(),
|
|
2408
|
+
clientMsgId,
|
|
2409
|
+
replyTo: resolvedReplyTo
|
|
2410
|
+
});
|
|
2411
|
+
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
2412
|
+
this.emit();
|
|
2413
|
+
}
|
|
2414
|
+
async materializeIfNeeded() {
|
|
2415
|
+
if (this._group) return this._group;
|
|
2416
|
+
if (!this._draft) throw new Error("messaging_not_configured");
|
|
2417
|
+
const group = await this.backend.materialize(this._draft);
|
|
2418
|
+
this._group = group;
|
|
2419
|
+
this.id = group.displayId;
|
|
2420
|
+
this._state = "active";
|
|
2421
|
+
this._draft = null;
|
|
2422
|
+
this.backend.registerActive(group);
|
|
2423
|
+
this.ensureWired();
|
|
2424
|
+
this.emit();
|
|
2425
|
+
return group;
|
|
2426
|
+
}
|
|
2427
|
+
// ── Membership (groups only) ──
|
|
2428
|
+
async addMemberUser(userId) {
|
|
2429
|
+
if (this.kind !== "group") throw new Error("chat_is_direct");
|
|
2430
|
+
const group = await this.materializeIfNeeded();
|
|
2431
|
+
await this.backend.addMember(group, userId);
|
|
2432
|
+
await this.refreshMembers();
|
|
2433
|
+
}
|
|
2434
|
+
async removeMemberUser(userId) {
|
|
2435
|
+
if (this.kind !== "group") throw new Error("chat_is_direct");
|
|
2436
|
+
if (!this._group) throw new Error("messaging_not_configured");
|
|
2437
|
+
await this.backend.removeMember(this._group, userId);
|
|
2438
|
+
await this.refreshMembers();
|
|
2439
|
+
}
|
|
2440
|
+
async leave() {
|
|
2441
|
+
if (!this._group) return;
|
|
2442
|
+
await this.backend.leave(this._group);
|
|
2443
|
+
this.liveUnsub?.();
|
|
2444
|
+
this.liveUnsub = null;
|
|
2445
|
+
}
|
|
2446
|
+
// ── Imperative ──
|
|
2447
|
+
setTyping(isTyping) {
|
|
2448
|
+
if (this._group) this.backend.setTyping(this._group, isTyping);
|
|
2449
|
+
}
|
|
2450
|
+
async markRead(message) {
|
|
2451
|
+
if (!this._group) return;
|
|
2452
|
+
await this.backend.markRead(this._group, message.serverSeq);
|
|
2453
|
+
this.readWatermark = Math.max(this.readWatermark, message.serverSeq);
|
|
2454
|
+
this.emit();
|
|
2455
|
+
}
|
|
2456
|
+
};
|
|
2457
|
+
|
|
2458
|
+
// src/messaging/delivery-source.ts
|
|
2459
|
+
var MessageHub = class {
|
|
2460
|
+
messageListeners = /* @__PURE__ */ new Map();
|
|
2461
|
+
convListeners = /* @__PURE__ */ new Map();
|
|
2462
|
+
onMessage(displayId, fn) {
|
|
2463
|
+
return this.add(this.messageListeners, displayId, fn);
|
|
2464
|
+
}
|
|
2465
|
+
emitMessage(displayId, m) {
|
|
2466
|
+
for (const fn of this.messageListeners.get(displayId) ?? []) fn(m);
|
|
2467
|
+
}
|
|
2468
|
+
onConv(displayId, fn) {
|
|
2469
|
+
return this.add(this.convListeners, displayId, fn);
|
|
2470
|
+
}
|
|
2471
|
+
emitConv(displayId, e) {
|
|
2472
|
+
for (const fn of this.convListeners.get(displayId) ?? []) fn(e);
|
|
2473
|
+
}
|
|
2474
|
+
add(map, key, fn) {
|
|
2475
|
+
let set = map.get(key);
|
|
2476
|
+
if (!set) {
|
|
2477
|
+
set = /* @__PURE__ */ new Set();
|
|
2478
|
+
map.set(key, set);
|
|
2479
|
+
}
|
|
2480
|
+
set.add(fn);
|
|
2481
|
+
return () => {
|
|
2482
|
+
set?.delete(fn);
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
};
|
|
2486
|
+
var MessageDeliverySource = class {
|
|
2487
|
+
constructor(rt, engine, hub, registry, messageStore, deviceId, selfUserId) {
|
|
2488
|
+
this.rt = rt;
|
|
2489
|
+
this.engine = engine;
|
|
2490
|
+
this.hub = hub;
|
|
2491
|
+
this.registry = registry;
|
|
2492
|
+
this.messageStore = messageStore;
|
|
2493
|
+
this.deviceId = deviceId;
|
|
2494
|
+
this.selfUserId = selfUserId;
|
|
2495
|
+
}
|
|
2496
|
+
rt;
|
|
2497
|
+
engine;
|
|
2498
|
+
hub;
|
|
2499
|
+
registry;
|
|
2500
|
+
messageStore;
|
|
2501
|
+
deviceId;
|
|
2502
|
+
selfUserId;
|
|
2503
|
+
started = false;
|
|
2504
|
+
wakeUnsub = null;
|
|
2505
|
+
// Per-observed-group conv subscription + heartbeat (presence/typing/read).
|
|
2506
|
+
observed = /* @__PURE__ */ new Map();
|
|
2507
|
+
/** Subscribe the device wake topic + run an initial drain. Idempotent. */
|
|
2508
|
+
async start() {
|
|
2509
|
+
if (this.started) return;
|
|
2510
|
+
this.started = true;
|
|
2511
|
+
try {
|
|
2512
|
+
const channel = this.rt.realtime.channel(`messaging:device:${this.deviceId}`);
|
|
2513
|
+
const sub = channel.on("new_message", () => {
|
|
2514
|
+
void this.pumpOnce();
|
|
2515
|
+
});
|
|
2516
|
+
const offConn = this.rt.realtime.status.onChange((s) => {
|
|
2517
|
+
if (s.state === "connected") void this.pumpOnce();
|
|
2518
|
+
});
|
|
2519
|
+
this.wakeUnsub = () => {
|
|
2520
|
+
sub.cancel();
|
|
2521
|
+
offConn();
|
|
2522
|
+
};
|
|
2523
|
+
} catch {
|
|
2524
|
+
}
|
|
2525
|
+
await this.pumpOnce();
|
|
2526
|
+
}
|
|
2527
|
+
stop() {
|
|
2528
|
+
this.wakeUnsub?.();
|
|
2529
|
+
this.wakeUnsub = null;
|
|
2530
|
+
for (const [, o] of this.observed) {
|
|
2531
|
+
o.unsub();
|
|
2532
|
+
if (o.heartbeat) clearInterval(o.heartbeat);
|
|
2533
|
+
}
|
|
2534
|
+
this.observed.clear();
|
|
2535
|
+
this.started = false;
|
|
2536
|
+
}
|
|
2537
|
+
/** One full drain: welcomes → queue (decrypt + emit) → ack. Never throws. */
|
|
2538
|
+
async pumpOnce() {
|
|
2539
|
+
try {
|
|
2540
|
+
await this.pullWelcomes();
|
|
2541
|
+
} catch {
|
|
2542
|
+
}
|
|
2543
|
+
try {
|
|
2544
|
+
await this.pullQueue();
|
|
2545
|
+
} catch {
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
// ── Welcomes ──
|
|
2549
|
+
async pullWelcomes() {
|
|
2550
|
+
const page = await palbeRequest(
|
|
2551
|
+
this.rt,
|
|
2552
|
+
"GET",
|
|
2553
|
+
MessagingPaths.deviceWelcomes(this.deviceId)
|
|
2554
|
+
);
|
|
2555
|
+
for (const w of page.welcomes) {
|
|
2556
|
+
try {
|
|
2557
|
+
const gidBytes = await this.engine.joinFromWelcome(fromBase64(w.welcome_b64));
|
|
2558
|
+
const group = {
|
|
2559
|
+
displayId: w.display_id,
|
|
2560
|
+
rfcGroupId: toBase64(gidBytes),
|
|
2561
|
+
currentEpoch: w.added_at_epoch,
|
|
2562
|
+
ownerUserId: null,
|
|
2563
|
+
directKey: null,
|
|
2564
|
+
name: null
|
|
2565
|
+
};
|
|
2566
|
+
this.registry.register(group);
|
|
2567
|
+
this.hub.emitMessage(group.displayId, {
|
|
2568
|
+
kind: "welcome",
|
|
2569
|
+
group,
|
|
2570
|
+
text: null,
|
|
2571
|
+
senderDeviceId: null,
|
|
2572
|
+
epoch: w.added_at_epoch,
|
|
2573
|
+
serverSeq: 0,
|
|
2574
|
+
receivedAt: /* @__PURE__ */ new Date(),
|
|
2575
|
+
clientMsgId: "",
|
|
2576
|
+
replyRef: null
|
|
2577
|
+
});
|
|
2578
|
+
} catch {
|
|
2579
|
+
const known = this.registry.all().some((g) => g.displayId === w.display_id);
|
|
2580
|
+
if (!known) {
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
// ── Queue ──
|
|
2586
|
+
async pullQueue() {
|
|
2587
|
+
const page = await palbeRequest(
|
|
2588
|
+
this.rt,
|
|
2589
|
+
"GET",
|
|
2590
|
+
MessagingPaths.deviceQueue(this.deviceId)
|
|
2591
|
+
);
|
|
2592
|
+
const ackIds = [];
|
|
2593
|
+
for (const row of page.messages) {
|
|
2594
|
+
if (await this.process(row)) ackIds.push(row.id);
|
|
2595
|
+
}
|
|
2596
|
+
if (ackIds.length > 0) {
|
|
2597
|
+
await palbeRequest(this.rt, "POST", MessagingPaths.deviceQueueAck(this.deviceId), {
|
|
2598
|
+
body: { message_ids: ackIds }
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
/** Process one queue row; returns true if consumed (should ack). */
|
|
2603
|
+
async process(row) {
|
|
2604
|
+
let blob;
|
|
2605
|
+
try {
|
|
2606
|
+
blob = fromBase64(row.ciphertext_b64);
|
|
2607
|
+
} catch {
|
|
2608
|
+
return true;
|
|
2609
|
+
}
|
|
2610
|
+
const group = (row.rfc_group_id_b64 ? this.registry.group(row.rfc_group_id_b64) : void 0) ?? this.registry.soleGroup();
|
|
2611
|
+
if (!group) return false;
|
|
2612
|
+
try {
|
|
2613
|
+
const received = await this.engine.processIncoming(fromBase64(group.rfcGroupId), blob);
|
|
2614
|
+
if (received.type === "application") {
|
|
2615
|
+
const { text, clientMsgId, replyTo } = decodeEnvelope(received.data);
|
|
2616
|
+
const stored = {
|
|
2617
|
+
id: `${group.rfcGroupId}#${row.server_seq}`,
|
|
2618
|
+
direction: "incoming",
|
|
2619
|
+
text,
|
|
2620
|
+
senderDeviceId: row.sender_device_id ?? null,
|
|
2621
|
+
epoch: row.epoch,
|
|
2622
|
+
serverSeq: row.server_seq,
|
|
2623
|
+
at: Date.now(),
|
|
2624
|
+
clientMsgId: clientMsgId || void 0,
|
|
2625
|
+
replyTo: replyTo ? {
|
|
2626
|
+
clientMsgId: replyTo.client_msg_id,
|
|
2627
|
+
previewBody: replyTo.preview?.body ?? null,
|
|
2628
|
+
previewAuthorUserId: replyTo.preview?.author_user_id ?? null,
|
|
2629
|
+
previewKind: replyTo.preview?.kind ?? "text"
|
|
2630
|
+
} : null
|
|
2631
|
+
};
|
|
2632
|
+
try {
|
|
2633
|
+
await this.messageStore.append(group.rfcGroupId, stored);
|
|
2634
|
+
} catch {
|
|
2635
|
+
return false;
|
|
2636
|
+
}
|
|
2637
|
+
this.hub.emitMessage(group.displayId, {
|
|
2638
|
+
kind: "application",
|
|
2639
|
+
group,
|
|
2640
|
+
text,
|
|
2641
|
+
senderDeviceId: row.sender_device_id ?? null,
|
|
2642
|
+
epoch: row.epoch,
|
|
2643
|
+
serverSeq: row.server_seq,
|
|
2644
|
+
receivedAt: /* @__PURE__ */ new Date(),
|
|
2645
|
+
clientMsgId,
|
|
2646
|
+
replyRef: replyTo
|
|
2647
|
+
});
|
|
2648
|
+
return true;
|
|
2649
|
+
}
|
|
2650
|
+
if (received.type === "commitApplied") {
|
|
2651
|
+
const newEpoch = Math.max(row.epoch, group.currentEpoch + 1);
|
|
2652
|
+
this.registry.bumpEpoch(group.rfcGroupId, newEpoch);
|
|
2653
|
+
const fresh = this.registry.group(group.rfcGroupId) ?? group;
|
|
2654
|
+
this.hub.emitMessage(group.displayId, {
|
|
2655
|
+
kind: "commit",
|
|
2656
|
+
group: fresh,
|
|
2657
|
+
text: null,
|
|
2658
|
+
senderDeviceId: row.sender_device_id ?? null,
|
|
2659
|
+
epoch: newEpoch,
|
|
2660
|
+
serverSeq: 0,
|
|
2661
|
+
receivedAt: /* @__PURE__ */ new Date(),
|
|
2662
|
+
clientMsgId: "",
|
|
2663
|
+
replyRef: null
|
|
2664
|
+
});
|
|
2665
|
+
return true;
|
|
2666
|
+
}
|
|
2667
|
+
return true;
|
|
2668
|
+
} catch (e) {
|
|
2669
|
+
if (isOwnEchoOrConsumed(e)) return true;
|
|
2670
|
+
return false;
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
// ── Conv topic (presence / typing / read) ──
|
|
2674
|
+
/** Begin observing a group's conv topic; fans events into the hub. Idempotent. */
|
|
2675
|
+
observeConversation(group) {
|
|
2676
|
+
if (this.observed.has(group.displayId)) return;
|
|
2677
|
+
try {
|
|
2678
|
+
const channel = this.rt.realtime.channel(`messaging:conv:${group.rfcGroupId}`);
|
|
2679
|
+
const subs = ["presence", "typing", "read"].map(
|
|
2680
|
+
(ev) => channel.on(ev, (payload) => {
|
|
2681
|
+
this.hub.emitConv(group.displayId, { event: ev, payload });
|
|
2682
|
+
})
|
|
2683
|
+
);
|
|
2684
|
+
const emitPresence = (online) => channel.send("presence", {
|
|
2685
|
+
user_id: this.selfUserId,
|
|
2686
|
+
device_id: this.deviceId,
|
|
2687
|
+
online,
|
|
2688
|
+
ts: Date.now() / 1e3
|
|
2689
|
+
});
|
|
2690
|
+
emitPresence(true);
|
|
2691
|
+
const heartbeat = setInterval(() => emitPresence(true), 25e3);
|
|
2692
|
+
this.observed.set(group.displayId, {
|
|
2693
|
+
unsub: () => {
|
|
2694
|
+
for (const s of subs) s.cancel();
|
|
2695
|
+
},
|
|
2696
|
+
heartbeat
|
|
2697
|
+
});
|
|
2698
|
+
} catch {
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
/** Announce typing on a group's conv topic (the app calls per keystroke). */
|
|
2702
|
+
setTyping(group, isTyping) {
|
|
2703
|
+
this.observeConversation(group);
|
|
2704
|
+
try {
|
|
2705
|
+
this.rt.realtime.channel(`messaging:conv:${group.rfcGroupId}`).send("typing", {
|
|
2706
|
+
user_id: this.selfUserId,
|
|
2707
|
+
device_id: this.deviceId,
|
|
2708
|
+
typing: isTyping
|
|
2709
|
+
});
|
|
2710
|
+
} catch {
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
/** Advance this device's read cursor (idempotent; the server clamps). */
|
|
2714
|
+
async markRead(group, upToServerSeq) {
|
|
2715
|
+
await palbeRequest(this.rt, "POST", MessagingPaths.groupRead(group.displayId), {
|
|
2716
|
+
body: { read_seq: upToServerSeq, read_epoch: group.currentEpoch, is_private: false }
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
};
|
|
2720
|
+
function isOwnEchoOrConsumed(e) {
|
|
2721
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2722
|
+
return msg.includes("message from self") || msg.includes("key not available, invalid generation");
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
// src/messaging/history.ts
|
|
2726
|
+
var MessageStore = class {
|
|
2727
|
+
constructor(kv) {
|
|
2728
|
+
this.kv = kv;
|
|
2729
|
+
}
|
|
2730
|
+
kv;
|
|
2731
|
+
key(rfcGroupId) {
|
|
2732
|
+
return `msgs:${rfcGroupId}`;
|
|
2733
|
+
}
|
|
2734
|
+
/** Append (idempotent on id) and persist the group's transcript. */
|
|
2735
|
+
async append(rfcGroupId, msg) {
|
|
2736
|
+
const list = await this.load(rfcGroupId);
|
|
2737
|
+
if (list.some((m) => m.id === msg.id)) return;
|
|
2738
|
+
list.push(msg);
|
|
2739
|
+
list.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
2740
|
+
await this.save(rfcGroupId, list);
|
|
2741
|
+
}
|
|
2742
|
+
/** True if a message id is already durable (the consumed-key recovery check). */
|
|
2743
|
+
async contains(rfcGroupId, id) {
|
|
2744
|
+
const list = await this.load(rfcGroupId);
|
|
2745
|
+
return list.some((m) => m.id === id);
|
|
2746
|
+
}
|
|
2747
|
+
/**
|
|
2748
|
+
* Read history for a group, newest-last. `before` pages older: returns up to
|
|
2749
|
+
* `limit` rows with serverSeq < before. nil `before` returns the newest page.
|
|
2750
|
+
*/
|
|
2751
|
+
async history(rfcGroupId, limit, before) {
|
|
2752
|
+
const list = await this.load(rfcGroupId);
|
|
2753
|
+
const filtered = before == null ? list : list.filter((m) => m.serverSeq < before);
|
|
2754
|
+
return filtered.slice(Math.max(0, filtered.length - limit));
|
|
2755
|
+
}
|
|
2756
|
+
async load(rfcGroupId) {
|
|
2757
|
+
const raw = await this.kv.get(this.key(rfcGroupId));
|
|
2758
|
+
if (!raw) return [];
|
|
2759
|
+
try {
|
|
2760
|
+
return JSON.parse(decodeUtf8(raw));
|
|
2761
|
+
} catch {
|
|
2762
|
+
return [];
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
async save(rfcGroupId, list) {
|
|
2766
|
+
await this.kv.set(this.key(rfcGroupId), encodeUtf8(JSON.stringify(list)));
|
|
2767
|
+
}
|
|
2768
|
+
async wipe() {
|
|
2769
|
+
for (const k of await this.kv.keys("msgs:")) await this.kv.delete(k);
|
|
2770
|
+
}
|
|
2771
|
+
};
|
|
2772
|
+
var GroupCatalog = class {
|
|
2773
|
+
constructor(kv) {
|
|
2774
|
+
this.kv = kv;
|
|
2775
|
+
}
|
|
2776
|
+
kv;
|
|
2777
|
+
async upsert(g) {
|
|
2778
|
+
await this.kv.set(`cat:${g.rfcGroupId}`, encodeUtf8(JSON.stringify(g)));
|
|
2779
|
+
}
|
|
2780
|
+
async remove(rfcGroupId) {
|
|
2781
|
+
await this.kv.delete(`cat:${rfcGroupId}`);
|
|
2782
|
+
}
|
|
2783
|
+
async all() {
|
|
2784
|
+
const keys = await this.kv.keys("cat:");
|
|
2785
|
+
const out = [];
|
|
2786
|
+
for (const k of keys) {
|
|
2787
|
+
const raw = await this.kv.get(k);
|
|
2788
|
+
if (!raw) continue;
|
|
2789
|
+
try {
|
|
2790
|
+
out.push(JSON.parse(decodeUtf8(raw)));
|
|
2791
|
+
} catch {
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
return out;
|
|
2795
|
+
}
|
|
2796
|
+
async wipe() {
|
|
2797
|
+
for (const k of await this.kv.keys("cat:")) await this.kv.delete(k);
|
|
2798
|
+
}
|
|
2799
|
+
};
|
|
2800
|
+
|
|
2801
|
+
// src/messaging/wasm/pkg/palbe_mls_bg.js
|
|
2802
|
+
var palbe_mls_bg_exports = {};
|
|
2803
|
+
__export(palbe_mls_bg_exports, {
|
|
2804
|
+
MlsClient: () => MlsClient,
|
|
2805
|
+
MlsGroup: () => MlsGroup,
|
|
2806
|
+
__wbg_BigInt_231999d28f899902: () => __wbg_BigInt_231999d28f899902,
|
|
2807
|
+
__wbg_Error_fdd633d4bb5dd76a: () => __wbg_Error_fdd633d4bb5dd76a,
|
|
2808
|
+
__wbg_String_8564e559799eccda: () => __wbg_String_8564e559799eccda,
|
|
2809
|
+
__wbg___wbindgen_bigint_get_as_i64_d9e915702856f831: () => __wbg___wbindgen_bigint_get_as_i64_d9e915702856f831,
|
|
2810
|
+
__wbg___wbindgen_debug_string_8a447059637473e2: () => __wbg___wbindgen_debug_string_8a447059637473e2,
|
|
2811
|
+
__wbg___wbindgen_is_function_acc5528be2b923f2: () => __wbg___wbindgen_is_function_acc5528be2b923f2,
|
|
2812
|
+
__wbg___wbindgen_is_null_6d937fbfb6478470: () => __wbg___wbindgen_is_null_6d937fbfb6478470,
|
|
2813
|
+
__wbg___wbindgen_is_object_0beba4a1980d3eea: () => __wbg___wbindgen_is_object_0beba4a1980d3eea,
|
|
2814
|
+
__wbg___wbindgen_is_string_1fca8072260dd261: () => __wbg___wbindgen_is_string_1fca8072260dd261,
|
|
2815
|
+
__wbg___wbindgen_is_undefined_721f8decd50c87a3: () => __wbg___wbindgen_is_undefined_721f8decd50c87a3,
|
|
2816
|
+
__wbg___wbindgen_jsval_eq_4e8c38722cb8ff51: () => __wbg___wbindgen_jsval_eq_4e8c38722cb8ff51,
|
|
2817
|
+
__wbg___wbindgen_memory_9751d9a3017e7c25: () => __wbg___wbindgen_memory_9751d9a3017e7c25,
|
|
2818
|
+
__wbg___wbindgen_number_get_1cc01dd708740256: () => __wbg___wbindgen_number_get_1cc01dd708740256,
|
|
2819
|
+
__wbg___wbindgen_string_get_71bb4348194e31f0: () => __wbg___wbindgen_string_get_71bb4348194e31f0,
|
|
2820
|
+
__wbg___wbindgen_throw_ea4887a5f8f9a9db: () => __wbg___wbindgen_throw_ea4887a5f8f9a9db,
|
|
2821
|
+
__wbg_buffer_7447b9cc2267a9e5: () => __wbg_buffer_7447b9cc2267a9e5,
|
|
2822
|
+
__wbg_call_67f43c91d09298f2: () => __wbg_call_67f43c91d09298f2,
|
|
2823
|
+
__wbg_call_b51415974987aa44: () => __wbg_call_b51415974987aa44,
|
|
2824
|
+
__wbg_crypto_38df2bab126b63dc: () => __wbg_crypto_38df2bab126b63dc,
|
|
2825
|
+
__wbg_delete_21fafcbc7bb82c85: () => __wbg_delete_21fafcbc7bb82c85,
|
|
2826
|
+
__wbg_epoch_15c000ffe3004541: () => __wbg_epoch_15c000ffe3004541,
|
|
2827
|
+
__wbg_error_a6fa202b58aa1cd3: () => __wbg_error_a6fa202b58aa1cd3,
|
|
2828
|
+
__wbg_getRandomValues_c44a50d8cfdaebeb: () => __wbg_getRandomValues_c44a50d8cfdaebeb,
|
|
2829
|
+
__wbg_get_615446055a48103f: () => __wbg_get_615446055a48103f,
|
|
2830
|
+
__wbg_globalThis_6d268067835e6709: () => __wbg_globalThis_6d268067835e6709,
|
|
2831
|
+
__wbg_global_3fe6c6c8ad6e6fb2: () => __wbg_global_3fe6c6c8ad6e6fb2,
|
|
2832
|
+
__wbg_insert_2d611f2bf9b60fee: () => __wbg_insert_2d611f2bf9b60fee,
|
|
2833
|
+
__wbg_instanceof_Error_38f854307ecab4ce: () => __wbg_instanceof_Error_38f854307ecab4ce,
|
|
2834
|
+
__wbg_length_c552db98817b9523: () => __wbg_length_c552db98817b9523,
|
|
2835
|
+
__wbg_maxEpochId_0b176768b2f352fb: () => __wbg_maxEpochId_0b176768b2f352fb,
|
|
2836
|
+
__wbg_message_a05fcf872473ffc5: () => __wbg_message_a05fcf872473ffc5,
|
|
2837
|
+
__wbg_msCrypto_bd5a034af96bcba6: () => __wbg_msCrypto_bd5a034af96bcba6,
|
|
2838
|
+
__wbg_new_227d7c05414eb861: () => __wbg_new_227d7c05414eb861,
|
|
2839
|
+
__wbg_new_32de5cbf49ca7dcb: () => __wbg_new_32de5cbf49ca7dcb,
|
|
2840
|
+
__wbg_new_364c96143b8f3496: () => __wbg_new_364c96143b8f3496,
|
|
2841
|
+
__wbg_new_b8b71ca8104fc178: () => __wbg_new_b8b71ca8104fc178,
|
|
2842
|
+
__wbg_new_d9762fd75876aafe: () => __wbg_new_d9762fd75876aafe,
|
|
2843
|
+
__wbg_new_no_args_4010ad257320fa4f: () => __wbg_new_no_args_4010ad257320fa4f,
|
|
2844
|
+
__wbg_new_with_byte_offset_and_length_8b21e3b1308deb48: () => __wbg_new_with_byte_offset_and_length_8b21e3b1308deb48,
|
|
2845
|
+
__wbg_new_with_length_5fdafe029be917a5: () => __wbg_new_with_length_5fdafe029be917a5,
|
|
2846
|
+
__wbg_node_84ea875411254db1: () => __wbg_node_84ea875411254db1,
|
|
2847
|
+
__wbg_process_44c7a14e11e9f69e: () => __wbg_process_44c7a14e11e9f69e,
|
|
2848
|
+
__wbg_push_1303ce035391aed3: () => __wbg_push_1303ce035391aed3,
|
|
2849
|
+
__wbg_randomFillSync_6c25eac9869eb53c: () => __wbg_randomFillSync_6c25eac9869eb53c,
|
|
2850
|
+
__wbg_require_b4edbdcf3e2a1ef0: () => __wbg_require_b4edbdcf3e2a1ef0,
|
|
2851
|
+
__wbg_self_1035a7cbd1b0d959: () => __wbg_self_1035a7cbd1b0d959,
|
|
2852
|
+
__wbg_set_047d1ea37bb67c19: () => __wbg_set_047d1ea37bb67c19,
|
|
2853
|
+
__wbg_set_1ddc4b8cd44d0da4: () => __wbg_set_1ddc4b8cd44d0da4,
|
|
2854
|
+
__wbg_set_28cba565792ec75f: () => __wbg_set_28cba565792ec75f,
|
|
2855
|
+
__wbg_set_6be42768c690e380: () => __wbg_set_6be42768c690e380,
|
|
2856
|
+
__wbg_set_name_b4c29a3a72ebbddc: () => __wbg_set_name_b4c29a3a72ebbddc,
|
|
2857
|
+
__wbg_set_wasm: () => __wbg_set_wasm,
|
|
2858
|
+
__wbg_stack_3b0d974bbf31e44f: () => __wbg_stack_3b0d974bbf31e44f,
|
|
2859
|
+
__wbg_state_bd20456c0f3efbc9: () => __wbg_state_bd20456c0f3efbc9,
|
|
2860
|
+
__wbg_subarray_e0162dcdea48eb3a: () => __wbg_subarray_e0162dcdea48eb3a,
|
|
2861
|
+
__wbg_versions_276b2795b1c6a219: () => __wbg_versions_276b2795b1c6a219,
|
|
2862
|
+
__wbg_window_9c17850b5e99c0ab: () => __wbg_window_9c17850b5e99c0ab,
|
|
2863
|
+
__wbg_write_56b1cf5bb0e780d6: () => __wbg_write_56b1cf5bb0e780d6,
|
|
2864
|
+
__wbindgen_cast_0000000000000001: () => __wbindgen_cast_0000000000000001,
|
|
2865
|
+
__wbindgen_cast_0000000000000002: () => __wbindgen_cast_0000000000000002,
|
|
2866
|
+
__wbindgen_cast_0000000000000003: () => __wbindgen_cast_0000000000000003,
|
|
2867
|
+
__wbindgen_init_externref_table: () => __wbindgen_init_externref_table,
|
|
2868
|
+
generateClient: () => generateClient,
|
|
2869
|
+
setPanicHook: () => setPanicHook
|
|
2870
|
+
});
|
|
2871
|
+
var MlsClient = class _MlsClient {
|
|
2872
|
+
static __wrap(ptr) {
|
|
2873
|
+
const obj = Object.create(_MlsClient.prototype);
|
|
2874
|
+
obj.__wbg_ptr = ptr;
|
|
2875
|
+
MlsClientFinalization.register(obj, obj.__wbg_ptr, obj);
|
|
2876
|
+
return obj;
|
|
2877
|
+
}
|
|
2878
|
+
__destroy_into_raw() {
|
|
2879
|
+
const ptr = this.__wbg_ptr;
|
|
2880
|
+
this.__wbg_ptr = 0;
|
|
2881
|
+
MlsClientFinalization.unregister(this);
|
|
2882
|
+
return ptr;
|
|
2883
|
+
}
|
|
2884
|
+
free() {
|
|
2885
|
+
const ptr = this.__destroy_into_raw();
|
|
2886
|
+
wasm.__wbg_mlsclient_free(ptr, 0);
|
|
2887
|
+
}
|
|
2888
|
+
/**
|
|
2889
|
+
* The MLS Capabilities wire bytes (the keydir's `capabilities`).
|
|
2890
|
+
* @returns {Uint8Array}
|
|
2891
|
+
*/
|
|
2892
|
+
capabilities() {
|
|
2893
|
+
const ret = wasm.mlsclient_capabilities(this.__wbg_ptr);
|
|
2894
|
+
var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
|
2895
|
+
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
|
2896
|
+
return v1;
|
|
2897
|
+
}
|
|
2898
|
+
/**
|
|
2899
|
+
* Create a new group at epoch 0. `groupId` `undefined`/`null` lets mls-rs
|
|
2900
|
+
* mint a globally-unique id (the rfc_group_id routing key).
|
|
2901
|
+
* @param {Uint8Array | null} [group_id]
|
|
2902
|
+
* @returns {MlsGroup}
|
|
2903
|
+
*/
|
|
2904
|
+
createGroup(group_id) {
|
|
2905
|
+
var ptr0 = isLikeNone(group_id) ? 0 : passArray8ToWasm0(group_id, wasm.__wbindgen_malloc);
|
|
2906
|
+
var len0 = WASM_VECTOR_LEN;
|
|
2907
|
+
const ret = wasm.mlsclient_createGroup(this.__wbg_ptr, ptr0, len0);
|
|
2908
|
+
if (ret[2]) {
|
|
2909
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
2910
|
+
}
|
|
2911
|
+
return MlsGroup.__wrap(ret[0]);
|
|
2912
|
+
}
|
|
2913
|
+
/**
|
|
2914
|
+
* The MLS Credential wire bytes (the keydir's `credential`).
|
|
2915
|
+
* @returns {Uint8Array}
|
|
2916
|
+
*/
|
|
2917
|
+
credential() {
|
|
2918
|
+
const ret = wasm.mlsclient_credential(this.__wbg_ptr);
|
|
2919
|
+
var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
|
2920
|
+
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
|
2921
|
+
return v1;
|
|
2922
|
+
}
|
|
2923
|
+
/**
|
|
2924
|
+
* The MLS-encoded enrollment material `{ signatureKey, credential,
|
|
2925
|
+
* capabilities }` (byte fields as arrays-of-numbers — wrap in Uint8Array as
|
|
2926
|
+
* needed). Base64 these into the SP-0 keydir enroll body.
|
|
2927
|
+
* @returns {any}
|
|
2928
|
+
*/
|
|
2929
|
+
enrollmentMaterial() {
|
|
2930
|
+
const ret = wasm.mlsclient_enrollmentMaterial(this.__wbg_ptr);
|
|
2931
|
+
if (ret[2]) {
|
|
2932
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
2933
|
+
}
|
|
2934
|
+
return takeFromExternrefTable0(ret[0]);
|
|
2935
|
+
}
|
|
2936
|
+
/**
|
|
2937
|
+
* Mint a one-use KeyPackage (RAW RFC 9420 §10 bytes) — uploaded to the
|
|
2938
|
+
* keydir, consumed by an adder's `addMember`.
|
|
2939
|
+
* @returns {Uint8Array}
|
|
2940
|
+
*/
|
|
2941
|
+
generateKeyPackage() {
|
|
2942
|
+
const ret = wasm.mlsclient_generateKeyPackage(this.__wbg_ptr);
|
|
2943
|
+
if (ret[3]) {
|
|
2944
|
+
throw takeFromExternrefTable0(ret[2]);
|
|
2945
|
+
}
|
|
2946
|
+
var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
|
2947
|
+
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
|
2948
|
+
return v1;
|
|
2949
|
+
}
|
|
2950
|
+
/**
|
|
2951
|
+
* Join a group from a Welcome (opaque MLSMessage bytes).
|
|
2952
|
+
* @param {Uint8Array} welcome
|
|
2953
|
+
* @returns {MlsGroup}
|
|
2954
|
+
*/
|
|
2955
|
+
joinGroup(welcome) {
|
|
2956
|
+
const ptr0 = passArray8ToWasm0(welcome, wasm.__wbindgen_malloc);
|
|
2957
|
+
const len0 = WASM_VECTOR_LEN;
|
|
2958
|
+
const ret = wasm.mlsclient_joinGroup(this.__wbg_ptr, ptr0, len0);
|
|
2959
|
+
if (ret[2]) {
|
|
2960
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
2961
|
+
}
|
|
2962
|
+
return MlsGroup.__wrap(ret[0]);
|
|
2963
|
+
}
|
|
2964
|
+
/**
|
|
2965
|
+
* Re-hydrate a previously-persisted group by its `groupId` (restart-safe
|
|
2966
|
+
* reload of an already-joined group).
|
|
2967
|
+
* @param {Uint8Array} group_id
|
|
2968
|
+
* @returns {MlsGroup}
|
|
2969
|
+
*/
|
|
2970
|
+
loadGroup(group_id) {
|
|
2971
|
+
const ptr0 = passArray8ToWasm0(group_id, wasm.__wbindgen_malloc);
|
|
2972
|
+
const len0 = WASM_VECTOR_LEN;
|
|
2973
|
+
const ret = wasm.mlsclient_loadGroup(this.__wbg_ptr, ptr0, len0);
|
|
2974
|
+
if (ret[2]) {
|
|
2975
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
2976
|
+
}
|
|
2977
|
+
return MlsGroup.__wrap(ret[0]);
|
|
2978
|
+
}
|
|
2979
|
+
/**
|
|
2980
|
+
* The serialized STABLE signature keypair (secret-bearing). Persist sealed
|
|
2981
|
+
* and pass back as `signatureKeypair` on restart so the device keeps the
|
|
2982
|
+
* SAME MLS signing identity.
|
|
2983
|
+
* @returns {Uint8Array}
|
|
2984
|
+
*/
|
|
2985
|
+
signatureKeypair() {
|
|
2986
|
+
const ret = wasm.mlsclient_signatureKeypair(this.__wbg_ptr);
|
|
2987
|
+
var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
|
2988
|
+
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
|
2989
|
+
return v1;
|
|
2990
|
+
}
|
|
2991
|
+
/**
|
|
2992
|
+
* The device's STABLE signature PUBLIC key bytes (the keydir's
|
|
2993
|
+
* `signature_key`).
|
|
2994
|
+
* @returns {Uint8Array}
|
|
2995
|
+
*/
|
|
2996
|
+
signaturePublicKey() {
|
|
2997
|
+
const ret = wasm.mlsclient_signaturePublicKey(this.__wbg_ptr);
|
|
2998
|
+
var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
|
2999
|
+
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
|
3000
|
+
return v1;
|
|
3001
|
+
}
|
|
3002
|
+
};
|
|
3003
|
+
if (Symbol.dispose) MlsClient.prototype[Symbol.dispose] = MlsClient.prototype.free;
|
|
3004
|
+
var MlsGroup = class _MlsGroup {
|
|
3005
|
+
static __wrap(ptr) {
|
|
3006
|
+
const obj = Object.create(_MlsGroup.prototype);
|
|
3007
|
+
obj.__wbg_ptr = ptr;
|
|
3008
|
+
MlsGroupFinalization.register(obj, obj.__wbg_ptr, obj);
|
|
3009
|
+
return obj;
|
|
3010
|
+
}
|
|
3011
|
+
__destroy_into_raw() {
|
|
3012
|
+
const ptr = this.__wbg_ptr;
|
|
3013
|
+
this.__wbg_ptr = 0;
|
|
3014
|
+
MlsGroupFinalization.unregister(this);
|
|
3015
|
+
return ptr;
|
|
3016
|
+
}
|
|
3017
|
+
free() {
|
|
3018
|
+
const ptr = this.__destroy_into_raw();
|
|
3019
|
+
wasm.__wbg_mlsgroup_free(ptr, 0);
|
|
3020
|
+
}
|
|
3021
|
+
/**
|
|
3022
|
+
* Stage a member add. Returns `{ commit, welcome?, addedLeafIndices }`
|
|
3023
|
+
* (byte fields as arrays-of-numbers). The group is PENDING after this.
|
|
3024
|
+
* @param {Uint8Array} key_package_msg
|
|
3025
|
+
* @returns {any}
|
|
3026
|
+
*/
|
|
3027
|
+
addMember(key_package_msg) {
|
|
3028
|
+
const ptr0 = passArray8ToWasm0(key_package_msg, wasm.__wbindgen_malloc);
|
|
3029
|
+
const len0 = WASM_VECTOR_LEN;
|
|
3030
|
+
const ret = wasm.mlsgroup_addMember(this.__wbg_ptr, ptr0, len0);
|
|
3031
|
+
if (ret[2]) {
|
|
3032
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
3033
|
+
}
|
|
3034
|
+
return takeFromExternrefTable0(ret[0]);
|
|
3035
|
+
}
|
|
3036
|
+
/**
|
|
3037
|
+
* Apply the staged commit (advances the local epoch). Call ONLY after the
|
|
3038
|
+
* server accepted the commit.
|
|
3039
|
+
*/
|
|
3040
|
+
applyPendingCommit() {
|
|
3041
|
+
const ret = wasm.mlsgroup_applyPendingCommit(this.__wbg_ptr);
|
|
3042
|
+
if (ret[1]) {
|
|
3043
|
+
throw takeFromExternrefTable0(ret[0]);
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
/**
|
|
3047
|
+
* Discard the staged commit without advancing (the 409-rebase path).
|
|
3048
|
+
*/
|
|
3049
|
+
clearPendingCommit() {
|
|
3050
|
+
wasm.mlsgroup_clearPendingCommit(this.__wbg_ptr);
|
|
3051
|
+
}
|
|
3052
|
+
/**
|
|
3053
|
+
* Stage a SINGLE atomic add+remove commit. `addKeyPackages` is an
|
|
3054
|
+
* `Array<Uint8Array>` of one-use key packages; `removeMemberIds` an
|
|
3055
|
+
* `Array<Uint8Array>` of member identity bytes. Returns the same
|
|
3056
|
+
* `AddResult` shape as `addMember`. PENDING after this.
|
|
3057
|
+
* @param {Uint8Array[]} add_key_packages
|
|
3058
|
+
* @param {Uint8Array[]} remove_member_ids
|
|
3059
|
+
* @returns {any}
|
|
3060
|
+
*/
|
|
3061
|
+
commitChanges(add_key_packages, remove_member_ids) {
|
|
3062
|
+
const ptr0 = passArrayJsValueToWasm0(add_key_packages, wasm.__wbindgen_malloc);
|
|
3063
|
+
const len0 = WASM_VECTOR_LEN;
|
|
3064
|
+
const ptr1 = passArrayJsValueToWasm0(remove_member_ids, wasm.__wbindgen_malloc);
|
|
3065
|
+
const len1 = WASM_VECTOR_LEN;
|
|
3066
|
+
const ret = wasm.mlsgroup_commitChanges(this.__wbg_ptr, ptr0, len0, ptr1, len1);
|
|
3067
|
+
if (ret[2]) {
|
|
3068
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
3069
|
+
}
|
|
3070
|
+
return takeFromExternrefTable0(ret[0]);
|
|
3071
|
+
}
|
|
3072
|
+
/**
|
|
3073
|
+
* The current (applied) epoch as a JS BigInt (u64 exceeds safe-integer).
|
|
3074
|
+
* @returns {bigint}
|
|
3075
|
+
*/
|
|
3076
|
+
currentEpoch() {
|
|
3077
|
+
const ret = wasm.mlsgroup_currentEpoch(this.__wbg_ptr);
|
|
3078
|
+
return BigInt.asUintN(64, ret);
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Inspect an incoming Commit and return the membership delta it WOULD
|
|
3082
|
+
* effect WITHOUT applying it (the reconciliation gate's crypto-truth).
|
|
3083
|
+
* Returns `{ kind, addedIdentities, removedIdentities }`.
|
|
3084
|
+
* @param {Uint8Array} commit_msg
|
|
3085
|
+
* @returns {any}
|
|
3086
|
+
*/
|
|
3087
|
+
describeIncomingCommit(commit_msg) {
|
|
3088
|
+
const ptr0 = passArray8ToWasm0(commit_msg, wasm.__wbindgen_malloc);
|
|
3089
|
+
const len0 = WASM_VECTOR_LEN;
|
|
3090
|
+
const ret = wasm.mlsgroup_describeIncomingCommit(this.__wbg_ptr, ptr0, len0);
|
|
3091
|
+
if (ret[2]) {
|
|
3092
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
3093
|
+
}
|
|
3094
|
+
return takeFromExternrefTable0(ret[0]);
|
|
3095
|
+
}
|
|
3096
|
+
/**
|
|
3097
|
+
* Encrypt an application message at the current epoch. Returns the opaque
|
|
3098
|
+
* MLSMessage bytes (PrivateMessage).
|
|
3099
|
+
* @param {Uint8Array} plaintext
|
|
3100
|
+
* @returns {Uint8Array}
|
|
3101
|
+
*/
|
|
3102
|
+
encryptApplicationMessage(plaintext) {
|
|
3103
|
+
const ptr0 = passArray8ToWasm0(plaintext, wasm.__wbindgen_malloc);
|
|
3104
|
+
const len0 = WASM_VECTOR_LEN;
|
|
3105
|
+
const ret = wasm.mlsgroup_encryptApplicationMessage(this.__wbg_ptr, ptr0, len0);
|
|
3106
|
+
if (ret[3]) {
|
|
3107
|
+
throw takeFromExternrefTable0(ret[2]);
|
|
3108
|
+
}
|
|
3109
|
+
var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
|
3110
|
+
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
|
3111
|
+
return v2;
|
|
3112
|
+
}
|
|
3113
|
+
/**
|
|
3114
|
+
* Derive the group's current-epoch exporter secret (RFC 9420 §8.5) — the SAME
|
|
3115
|
+
* engine call iOS uses (uniffi `exportSecret`), so a cross-platform call/group
|
|
3116
|
+
* derives a byte-identical key. The SP-4 web call sets this as the LiveKit
|
|
3117
|
+
* frame key (server + SFU stay blind); also group-name/media sealing.
|
|
3118
|
+
* @param {Uint8Array} label
|
|
3119
|
+
* @param {Uint8Array} context
|
|
3120
|
+
* @param {bigint} len
|
|
3121
|
+
* @returns {Uint8Array}
|
|
3122
|
+
*/
|
|
3123
|
+
exportSecret(label, context, len) {
|
|
3124
|
+
const ptr0 = passArray8ToWasm0(label, wasm.__wbindgen_malloc);
|
|
3125
|
+
const len0 = WASM_VECTOR_LEN;
|
|
3126
|
+
const ptr1 = passArray8ToWasm0(context, wasm.__wbindgen_malloc);
|
|
3127
|
+
const len1 = WASM_VECTOR_LEN;
|
|
3128
|
+
const ret = wasm.mlsgroup_exportSecret(this.__wbg_ptr, ptr0, len0, ptr1, len1, len);
|
|
3129
|
+
if (ret[3]) {
|
|
3130
|
+
throw takeFromExternrefTable0(ret[2]);
|
|
3131
|
+
}
|
|
3132
|
+
var v3 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
|
3133
|
+
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
|
3134
|
+
return v3;
|
|
3135
|
+
}
|
|
3136
|
+
/**
|
|
3137
|
+
* The MLS group id (the rfc_group_id routing key / storage key).
|
|
3138
|
+
* @returns {Uint8Array}
|
|
3139
|
+
*/
|
|
3140
|
+
groupId() {
|
|
3141
|
+
const ret = wasm.mlsgroup_groupId(this.__wbg_ptr);
|
|
3142
|
+
var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
|
3143
|
+
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
|
3144
|
+
return v1;
|
|
3145
|
+
}
|
|
3146
|
+
/**
|
|
3147
|
+
* True while a staged (unapplied) commit exists.
|
|
3148
|
+
* @returns {boolean}
|
|
3149
|
+
*/
|
|
3150
|
+
hasPendingCommit() {
|
|
3151
|
+
const ret = wasm.mlsgroup_hasPendingCommit(this.__wbg_ptr);
|
|
3152
|
+
return ret !== 0;
|
|
3153
|
+
}
|
|
3154
|
+
/**
|
|
3155
|
+
* Process an incoming MLSMessage and apply its effect. Returns a
|
|
3156
|
+
* `ReceivedMessage` plain object (serde-tagged enum): e.g.
|
|
3157
|
+
* `{ Application: { sender, data } }`, `{ CommitApplied: { epoch,
|
|
3158
|
+
* removedSelf } }`, `"Proposal"`, or `"Other"`.
|
|
3159
|
+
* @param {Uint8Array} message
|
|
3160
|
+
* @returns {any}
|
|
3161
|
+
*/
|
|
3162
|
+
processIncomingMessage(message) {
|
|
3163
|
+
const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
|
|
3164
|
+
const len0 = WASM_VECTOR_LEN;
|
|
3165
|
+
const ret = wasm.mlsgroup_processIncomingMessage(this.__wbg_ptr, ptr0, len0);
|
|
3166
|
+
if (ret[2]) {
|
|
3167
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
3168
|
+
}
|
|
3169
|
+
return takeFromExternrefTable0(ret[0]);
|
|
3170
|
+
}
|
|
3171
|
+
/**
|
|
3172
|
+
* Stage a member remove (by the member's identity bytes). Returns
|
|
3173
|
+
* `{ commit }`. PENDING after this.
|
|
3174
|
+
* @param {Uint8Array} member_id
|
|
3175
|
+
* @returns {any}
|
|
3176
|
+
*/
|
|
3177
|
+
removeMember(member_id) {
|
|
3178
|
+
const ptr0 = passArray8ToWasm0(member_id, wasm.__wbindgen_malloc);
|
|
3179
|
+
const len0 = WASM_VECTOR_LEN;
|
|
3180
|
+
const ret = wasm.mlsgroup_removeMember(this.__wbg_ptr, ptr0, len0);
|
|
3181
|
+
if (ret[2]) {
|
|
3182
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
3183
|
+
}
|
|
3184
|
+
return takeFromExternrefTable0(ret[0]);
|
|
3185
|
+
}
|
|
3186
|
+
/**
|
|
3187
|
+
* Persist the current group state through the JS group storage callback.
|
|
3188
|
+
*/
|
|
3189
|
+
writeToStorage() {
|
|
3190
|
+
const ret = wasm.mlsgroup_writeToStorage(this.__wbg_ptr);
|
|
3191
|
+
if (ret[1]) {
|
|
3192
|
+
throw takeFromExternrefTable0(ret[0]);
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
};
|
|
3196
|
+
if (Symbol.dispose) MlsGroup.prototype[Symbol.dispose] = MlsGroup.prototype.free;
|
|
3197
|
+
function generateClient(id, group_storage, key_package_storage, signature_keypair) {
|
|
3198
|
+
const ptr0 = passArray8ToWasm0(id, wasm.__wbindgen_malloc);
|
|
3199
|
+
const len0 = WASM_VECTOR_LEN;
|
|
3200
|
+
var ptr1 = isLikeNone(signature_keypair) ? 0 : passArray8ToWasm0(signature_keypair, wasm.__wbindgen_malloc);
|
|
3201
|
+
var len1 = WASM_VECTOR_LEN;
|
|
3202
|
+
const ret = wasm.generateClient(ptr0, len0, group_storage, key_package_storage, ptr1, len1);
|
|
3203
|
+
if (ret[2]) {
|
|
3204
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
3205
|
+
}
|
|
3206
|
+
return MlsClient.__wrap(ret[0]);
|
|
3207
|
+
}
|
|
3208
|
+
function setPanicHook() {
|
|
3209
|
+
wasm.setPanicHook();
|
|
3210
|
+
}
|
|
3211
|
+
function __wbg_BigInt_231999d28f899902() {
|
|
3212
|
+
return handleError(function(arg0) {
|
|
3213
|
+
const ret = BigInt(arg0);
|
|
3214
|
+
return ret;
|
|
3215
|
+
}, arguments);
|
|
3216
|
+
}
|
|
3217
|
+
function __wbg_Error_fdd633d4bb5dd76a(arg0, arg1) {
|
|
3218
|
+
const ret = Error(getStringFromWasm0(arg0, arg1));
|
|
3219
|
+
return ret;
|
|
3220
|
+
}
|
|
3221
|
+
function __wbg_String_8564e559799eccda(arg0, arg1) {
|
|
3222
|
+
const ret = String(arg1);
|
|
3223
|
+
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
3224
|
+
const len1 = WASM_VECTOR_LEN;
|
|
3225
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
3226
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
3227
|
+
}
|
|
3228
|
+
function __wbg___wbindgen_bigint_get_as_i64_d9e915702856f831(arg0, arg1) {
|
|
3229
|
+
const v = arg1;
|
|
3230
|
+
const ret = typeof v === "bigint" ? v : void 0;
|
|
3231
|
+
getDataViewMemory0().setBigInt64(arg0 + 8 * 1, isLikeNone(ret) ? BigInt(0) : ret, true);
|
|
3232
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
|
|
3233
|
+
}
|
|
3234
|
+
function __wbg___wbindgen_debug_string_8a447059637473e2(arg0, arg1) {
|
|
3235
|
+
const ret = debugString(arg1);
|
|
3236
|
+
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
3237
|
+
const len1 = WASM_VECTOR_LEN;
|
|
3238
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
3239
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
3240
|
+
}
|
|
3241
|
+
function __wbg___wbindgen_is_function_acc5528be2b923f2(arg0) {
|
|
3242
|
+
const ret = typeof arg0 === "function";
|
|
3243
|
+
return ret;
|
|
3244
|
+
}
|
|
3245
|
+
function __wbg___wbindgen_is_null_6d937fbfb6478470(arg0) {
|
|
3246
|
+
const ret = arg0 === null;
|
|
3247
|
+
return ret;
|
|
3248
|
+
}
|
|
3249
|
+
function __wbg___wbindgen_is_object_0beba4a1980d3eea(arg0) {
|
|
3250
|
+
const val = arg0;
|
|
3251
|
+
const ret = typeof val === "object" && val !== null;
|
|
3252
|
+
return ret;
|
|
3253
|
+
}
|
|
3254
|
+
function __wbg___wbindgen_is_string_1fca8072260dd261(arg0) {
|
|
3255
|
+
const ret = typeof arg0 === "string";
|
|
3256
|
+
return ret;
|
|
3257
|
+
}
|
|
3258
|
+
function __wbg___wbindgen_is_undefined_721f8decd50c87a3(arg0) {
|
|
3259
|
+
const ret = arg0 === void 0;
|
|
3260
|
+
return ret;
|
|
3261
|
+
}
|
|
3262
|
+
function __wbg___wbindgen_jsval_eq_4e8c38722cb8ff51(arg0, arg1) {
|
|
3263
|
+
const ret = arg0 === arg1;
|
|
3264
|
+
return ret;
|
|
3265
|
+
}
|
|
3266
|
+
function __wbg___wbindgen_memory_9751d9a3017e7c25() {
|
|
3267
|
+
const ret = wasm.memory;
|
|
3268
|
+
return ret;
|
|
3269
|
+
}
|
|
3270
|
+
function __wbg___wbindgen_number_get_1cc01dd708740256(arg0, arg1) {
|
|
3271
|
+
const obj = arg1;
|
|
3272
|
+
const ret = typeof obj === "number" ? obj : void 0;
|
|
3273
|
+
getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
|
|
3274
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
|
|
3275
|
+
}
|
|
3276
|
+
function __wbg___wbindgen_string_get_71bb4348194e31f0(arg0, arg1) {
|
|
3277
|
+
const obj = arg1;
|
|
3278
|
+
const ret = typeof obj === "string" ? obj : void 0;
|
|
3279
|
+
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
3280
|
+
var len1 = WASM_VECTOR_LEN;
|
|
3281
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
3282
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
3283
|
+
}
|
|
3284
|
+
function __wbg___wbindgen_throw_ea4887a5f8f9a9db(arg0, arg1) {
|
|
3285
|
+
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
3286
|
+
}
|
|
3287
|
+
function __wbg_buffer_7447b9cc2267a9e5(arg0) {
|
|
3288
|
+
const ret = arg0.buffer;
|
|
3289
|
+
return ret;
|
|
3290
|
+
}
|
|
3291
|
+
function __wbg_call_67f43c91d09298f2() {
|
|
3292
|
+
return handleError(function(arg0, arg1, arg2) {
|
|
3293
|
+
const ret = arg0.call(arg1, arg2);
|
|
3294
|
+
return ret;
|
|
3295
|
+
}, arguments);
|
|
3296
|
+
}
|
|
3297
|
+
function __wbg_call_b51415974987aa44() {
|
|
3298
|
+
return handleError(function(arg0, arg1) {
|
|
3299
|
+
const ret = arg0.call(arg1);
|
|
3300
|
+
return ret;
|
|
3301
|
+
}, arguments);
|
|
3302
|
+
}
|
|
3303
|
+
function __wbg_crypto_38df2bab126b63dc(arg0) {
|
|
3304
|
+
const ret = arg0.crypto;
|
|
3305
|
+
return ret;
|
|
3306
|
+
}
|
|
3307
|
+
function __wbg_delete_21fafcbc7bb82c85() {
|
|
3308
|
+
return handleError(function(arg0, arg1) {
|
|
3309
|
+
arg0.delete(arg1);
|
|
3310
|
+
}, arguments);
|
|
3311
|
+
}
|
|
3312
|
+
function __wbg_epoch_15c000ffe3004541() {
|
|
3313
|
+
return handleError(function(arg0, arg1, arg2) {
|
|
3314
|
+
const ret = arg0.epoch(arg1, BigInt.asUintN(64, arg2));
|
|
3315
|
+
return ret;
|
|
3316
|
+
}, arguments);
|
|
3317
|
+
}
|
|
3318
|
+
function __wbg_error_a6fa202b58aa1cd3(arg0, arg1) {
|
|
3319
|
+
let deferred0_0;
|
|
3320
|
+
let deferred0_1;
|
|
3321
|
+
try {
|
|
3322
|
+
deferred0_0 = arg0;
|
|
3323
|
+
deferred0_1 = arg1;
|
|
3324
|
+
console.error(getStringFromWasm0(arg0, arg1));
|
|
3325
|
+
} finally {
|
|
3326
|
+
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
function __wbg_getRandomValues_c44a50d8cfdaebeb() {
|
|
3330
|
+
return handleError(function(arg0, arg1) {
|
|
3331
|
+
arg0.getRandomValues(arg1);
|
|
3332
|
+
}, arguments);
|
|
3333
|
+
}
|
|
3334
|
+
function __wbg_get_615446055a48103f() {
|
|
3335
|
+
return handleError(function(arg0, arg1) {
|
|
3336
|
+
const ret = arg0.get(arg1);
|
|
3337
|
+
return ret;
|
|
3338
|
+
}, arguments);
|
|
3339
|
+
}
|
|
3340
|
+
function __wbg_globalThis_6d268067835e6709() {
|
|
3341
|
+
return handleError(function() {
|
|
3342
|
+
const ret = globalThis.globalThis;
|
|
3343
|
+
return ret;
|
|
3344
|
+
}, arguments);
|
|
3345
|
+
}
|
|
3346
|
+
function __wbg_global_3fe6c6c8ad6e6fb2() {
|
|
3347
|
+
return handleError(function() {
|
|
3348
|
+
const ret = global.global;
|
|
3349
|
+
return ret;
|
|
3350
|
+
}, arguments);
|
|
3351
|
+
}
|
|
3352
|
+
function __wbg_insert_2d611f2bf9b60fee() {
|
|
3353
|
+
return handleError(function(arg0, arg1, arg2) {
|
|
3354
|
+
arg0.insert(arg1, arg2);
|
|
3355
|
+
}, arguments);
|
|
3356
|
+
}
|
|
3357
|
+
function __wbg_instanceof_Error_38f854307ecab4ce(arg0) {
|
|
3358
|
+
let result;
|
|
3359
|
+
try {
|
|
3360
|
+
result = arg0 instanceof Error;
|
|
3361
|
+
} catch (_) {
|
|
3362
|
+
result = false;
|
|
3363
|
+
}
|
|
3364
|
+
const ret = result;
|
|
3365
|
+
return ret;
|
|
3366
|
+
}
|
|
3367
|
+
function __wbg_length_c552db98817b9523(arg0) {
|
|
3368
|
+
const ret = arg0.length;
|
|
3369
|
+
return ret;
|
|
3370
|
+
}
|
|
3371
|
+
function __wbg_maxEpochId_0b176768b2f352fb() {
|
|
3372
|
+
return handleError(function(arg0, arg1) {
|
|
3373
|
+
const ret = arg0.maxEpochId(arg1);
|
|
3374
|
+
return ret;
|
|
3375
|
+
}, arguments);
|
|
3376
|
+
}
|
|
3377
|
+
function __wbg_message_a05fcf872473ffc5(arg0) {
|
|
3378
|
+
const ret = arg0.message;
|
|
3379
|
+
return ret;
|
|
3380
|
+
}
|
|
3381
|
+
function __wbg_msCrypto_bd5a034af96bcba6(arg0) {
|
|
3382
|
+
const ret = arg0.msCrypto;
|
|
3383
|
+
return ret;
|
|
3384
|
+
}
|
|
3385
|
+
function __wbg_new_227d7c05414eb861() {
|
|
3386
|
+
const ret = new Error();
|
|
3387
|
+
return ret;
|
|
3388
|
+
}
|
|
3389
|
+
function __wbg_new_32de5cbf49ca7dcb(arg0) {
|
|
3390
|
+
const ret = new Uint8Array(arg0);
|
|
3391
|
+
return ret;
|
|
3392
|
+
}
|
|
3393
|
+
function __wbg_new_364c96143b8f3496() {
|
|
3394
|
+
const ret = new Object();
|
|
3395
|
+
return ret;
|
|
3396
|
+
}
|
|
3397
|
+
function __wbg_new_b8b71ca8104fc178(arg0, arg1) {
|
|
3398
|
+
const ret = new Error(getStringFromWasm0(arg0, arg1));
|
|
3399
|
+
return ret;
|
|
3400
|
+
}
|
|
3401
|
+
function __wbg_new_d9762fd75876aafe() {
|
|
3402
|
+
const ret = new Array();
|
|
3403
|
+
return ret;
|
|
3404
|
+
}
|
|
3405
|
+
function __wbg_new_no_args_4010ad257320fa4f(arg0, arg1) {
|
|
3406
|
+
const ret = new Function(getStringFromWasm0(arg0, arg1));
|
|
3407
|
+
return ret;
|
|
3408
|
+
}
|
|
3409
|
+
function __wbg_new_with_byte_offset_and_length_8b21e3b1308deb48(arg0, arg1, arg2) {
|
|
3410
|
+
const ret = new Uint8Array(arg0, arg1 >>> 0, arg2 >>> 0);
|
|
3411
|
+
return ret;
|
|
3412
|
+
}
|
|
3413
|
+
function __wbg_new_with_length_5fdafe029be917a5(arg0) {
|
|
3414
|
+
const ret = new Uint8Array(arg0 >>> 0);
|
|
3415
|
+
return ret;
|
|
3416
|
+
}
|
|
3417
|
+
function __wbg_node_84ea875411254db1(arg0) {
|
|
3418
|
+
const ret = arg0.node;
|
|
3419
|
+
return ret;
|
|
3420
|
+
}
|
|
3421
|
+
function __wbg_process_44c7a14e11e9f69e(arg0) {
|
|
3422
|
+
const ret = arg0.process;
|
|
3423
|
+
return ret;
|
|
3424
|
+
}
|
|
3425
|
+
function __wbg_push_1303ce035391aed3(arg0, arg1) {
|
|
3426
|
+
const ret = arg0.push(arg1);
|
|
3427
|
+
return ret;
|
|
3428
|
+
}
|
|
3429
|
+
function __wbg_randomFillSync_6c25eac9869eb53c() {
|
|
3430
|
+
return handleError(function(arg0, arg1) {
|
|
3431
|
+
arg0.randomFillSync(arg1);
|
|
3432
|
+
}, arguments);
|
|
3433
|
+
}
|
|
3434
|
+
function __wbg_require_b4edbdcf3e2a1ef0() {
|
|
3435
|
+
return handleError(function() {
|
|
3436
|
+
const ret = module.require;
|
|
3437
|
+
return ret;
|
|
3438
|
+
}, arguments);
|
|
3439
|
+
}
|
|
3440
|
+
function __wbg_self_1035a7cbd1b0d959() {
|
|
3441
|
+
return handleError(function() {
|
|
3442
|
+
const ret = self.self;
|
|
3443
|
+
return ret;
|
|
3444
|
+
}, arguments);
|
|
3445
|
+
}
|
|
3446
|
+
function __wbg_set_047d1ea37bb67c19(arg0, arg1, arg2) {
|
|
3447
|
+
arg0.set(arg1, arg2 >>> 0);
|
|
3448
|
+
}
|
|
3449
|
+
function __wbg_set_1ddc4b8cd44d0da4() {
|
|
3450
|
+
return handleError(function(arg0, arg1, arg2) {
|
|
3451
|
+
const ret = Reflect.set(arg0, arg1, arg2);
|
|
3452
|
+
return ret;
|
|
3453
|
+
}, arguments);
|
|
3454
|
+
}
|
|
3455
|
+
function __wbg_set_28cba565792ec75f(arg0, arg1, arg2) {
|
|
3456
|
+
arg0[arg1 >>> 0] = arg2;
|
|
3457
|
+
}
|
|
3458
|
+
function __wbg_set_6be42768c690e380(arg0, arg1, arg2) {
|
|
3459
|
+
arg0[arg1] = arg2;
|
|
3460
|
+
}
|
|
3461
|
+
function __wbg_set_name_b4c29a3a72ebbddc(arg0, arg1, arg2) {
|
|
3462
|
+
arg0.name = getStringFromWasm0(arg1, arg2);
|
|
3463
|
+
}
|
|
3464
|
+
function __wbg_stack_3b0d974bbf31e44f(arg0, arg1) {
|
|
3465
|
+
const ret = arg1.stack;
|
|
3466
|
+
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
3467
|
+
const len1 = WASM_VECTOR_LEN;
|
|
3468
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
3469
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
3470
|
+
}
|
|
3471
|
+
function __wbg_state_bd20456c0f3efbc9() {
|
|
3472
|
+
return handleError(function(arg0, arg1) {
|
|
3473
|
+
const ret = arg0.state(arg1);
|
|
3474
|
+
return ret;
|
|
3475
|
+
}, arguments);
|
|
3476
|
+
}
|
|
3477
|
+
function __wbg_subarray_e0162dcdea48eb3a(arg0, arg1, arg2) {
|
|
3478
|
+
const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0);
|
|
3479
|
+
return ret;
|
|
3480
|
+
}
|
|
3481
|
+
function __wbg_versions_276b2795b1c6a219(arg0) {
|
|
3482
|
+
const ret = arg0.versions;
|
|
3483
|
+
return ret;
|
|
3484
|
+
}
|
|
3485
|
+
function __wbg_window_9c17850b5e99c0ab() {
|
|
3486
|
+
return handleError(function() {
|
|
3487
|
+
const ret = window.window;
|
|
3488
|
+
return ret;
|
|
3489
|
+
}, arguments);
|
|
3490
|
+
}
|
|
3491
|
+
function __wbg_write_56b1cf5bb0e780d6() {
|
|
3492
|
+
return handleError(function(arg0, arg1, arg2, arg3, arg4) {
|
|
3493
|
+
arg0.write(arg1, arg2, arg3, arg4);
|
|
3494
|
+
}, arguments);
|
|
3495
|
+
}
|
|
3496
|
+
function __wbindgen_cast_0000000000000001(arg0) {
|
|
3497
|
+
const ret = arg0;
|
|
3498
|
+
return ret;
|
|
3499
|
+
}
|
|
3500
|
+
function __wbindgen_cast_0000000000000002(arg0, arg1) {
|
|
3501
|
+
const ret = getStringFromWasm0(arg0, arg1);
|
|
3502
|
+
return ret;
|
|
3503
|
+
}
|
|
3504
|
+
function __wbindgen_cast_0000000000000003(arg0) {
|
|
3505
|
+
const ret = BigInt.asUintN(64, arg0);
|
|
3506
|
+
return ret;
|
|
3507
|
+
}
|
|
3508
|
+
function __wbindgen_init_externref_table() {
|
|
3509
|
+
const table = wasm.__wbindgen_externrefs;
|
|
3510
|
+
const offset = table.grow(4);
|
|
3511
|
+
table.set(0, void 0);
|
|
3512
|
+
table.set(offset + 0, void 0);
|
|
3513
|
+
table.set(offset + 1, null);
|
|
3514
|
+
table.set(offset + 2, true);
|
|
3515
|
+
table.set(offset + 3, false);
|
|
3516
|
+
}
|
|
3517
|
+
var MlsClientFinalization = typeof FinalizationRegistry === "undefined" ? { register: () => {
|
|
3518
|
+
}, unregister: () => {
|
|
3519
|
+
} } : new FinalizationRegistry((ptr) => wasm.__wbg_mlsclient_free(ptr, 1));
|
|
3520
|
+
var MlsGroupFinalization = typeof FinalizationRegistry === "undefined" ? { register: () => {
|
|
3521
|
+
}, unregister: () => {
|
|
3522
|
+
} } : new FinalizationRegistry((ptr) => wasm.__wbg_mlsgroup_free(ptr, 1));
|
|
3523
|
+
function addToExternrefTable0(obj) {
|
|
3524
|
+
const idx = wasm.__externref_table_alloc();
|
|
3525
|
+
wasm.__wbindgen_externrefs.set(idx, obj);
|
|
3526
|
+
return idx;
|
|
3527
|
+
}
|
|
3528
|
+
function debugString(val) {
|
|
3529
|
+
const type = typeof val;
|
|
3530
|
+
if (type == "number" || type == "boolean" || val == null) {
|
|
3531
|
+
return `${val}`;
|
|
3532
|
+
}
|
|
3533
|
+
if (type == "string") {
|
|
3534
|
+
return `"${val}"`;
|
|
3535
|
+
}
|
|
3536
|
+
if (type == "symbol") {
|
|
3537
|
+
const description = val.description;
|
|
3538
|
+
if (description == null) {
|
|
3539
|
+
return "Symbol";
|
|
3540
|
+
} else {
|
|
3541
|
+
return `Symbol(${description})`;
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
if (type == "function") {
|
|
3545
|
+
const name = val.name;
|
|
3546
|
+
if (typeof name == "string" && name.length > 0) {
|
|
3547
|
+
return `Function(${name})`;
|
|
3548
|
+
} else {
|
|
3549
|
+
return "Function";
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
if (Array.isArray(val)) {
|
|
3553
|
+
const length = val.length;
|
|
3554
|
+
let debug = "[";
|
|
3555
|
+
if (length > 0) {
|
|
3556
|
+
debug += debugString(val[0]);
|
|
3557
|
+
}
|
|
3558
|
+
for (let i = 1; i < length; i++) {
|
|
3559
|
+
debug += ", " + debugString(val[i]);
|
|
3560
|
+
}
|
|
3561
|
+
debug += "]";
|
|
3562
|
+
return debug;
|
|
3563
|
+
}
|
|
3564
|
+
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
|
|
3565
|
+
let className;
|
|
3566
|
+
if (builtInMatches && builtInMatches.length > 1) {
|
|
3567
|
+
className = builtInMatches[1];
|
|
3568
|
+
} else {
|
|
3569
|
+
return toString.call(val);
|
|
3570
|
+
}
|
|
3571
|
+
if (className == "Object") {
|
|
3572
|
+
try {
|
|
3573
|
+
return "Object(" + JSON.stringify(val) + ")";
|
|
3574
|
+
} catch (_) {
|
|
3575
|
+
return "Object";
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
if (val instanceof Error) {
|
|
3579
|
+
return `${val.name}: ${val.message}
|
|
3580
|
+
${val.stack}`;
|
|
3581
|
+
}
|
|
3582
|
+
return className;
|
|
3583
|
+
}
|
|
3584
|
+
function getArrayU8FromWasm0(ptr, len) {
|
|
3585
|
+
ptr = ptr >>> 0;
|
|
3586
|
+
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
|
3587
|
+
}
|
|
3588
|
+
var cachedDataViewMemory0 = null;
|
|
3589
|
+
function getDataViewMemory0() {
|
|
3590
|
+
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || cachedDataViewMemory0.buffer.detached === void 0 && cachedDataViewMemory0.buffer !== wasm.memory.buffer) {
|
|
3591
|
+
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
|
3592
|
+
}
|
|
3593
|
+
return cachedDataViewMemory0;
|
|
3594
|
+
}
|
|
3595
|
+
function getStringFromWasm0(ptr, len) {
|
|
3596
|
+
return decodeText(ptr >>> 0, len);
|
|
3597
|
+
}
|
|
3598
|
+
var cachedUint8ArrayMemory0 = null;
|
|
3599
|
+
function getUint8ArrayMemory0() {
|
|
3600
|
+
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
|
3601
|
+
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
|
3602
|
+
}
|
|
3603
|
+
return cachedUint8ArrayMemory0;
|
|
3604
|
+
}
|
|
3605
|
+
function handleError(f, args) {
|
|
3606
|
+
try {
|
|
3607
|
+
return f.apply(this, args);
|
|
3608
|
+
} catch (e) {
|
|
3609
|
+
const idx = addToExternrefTable0(e);
|
|
3610
|
+
wasm.__wbindgen_exn_store(idx);
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
function isLikeNone(x) {
|
|
3614
|
+
return x === void 0 || x === null;
|
|
3615
|
+
}
|
|
3616
|
+
function passArray8ToWasm0(arg, malloc) {
|
|
3617
|
+
const ptr = malloc(arg.length * 1, 1) >>> 0;
|
|
3618
|
+
getUint8ArrayMemory0().set(arg, ptr / 1);
|
|
3619
|
+
WASM_VECTOR_LEN = arg.length;
|
|
3620
|
+
return ptr;
|
|
3621
|
+
}
|
|
3622
|
+
function passArrayJsValueToWasm0(array, malloc) {
|
|
3623
|
+
const ptr = malloc(array.length * 4, 4) >>> 0;
|
|
3624
|
+
for (let i = 0; i < array.length; i++) {
|
|
3625
|
+
const add = addToExternrefTable0(array[i]);
|
|
3626
|
+
getDataViewMemory0().setUint32(ptr + 4 * i, add, true);
|
|
3627
|
+
}
|
|
3628
|
+
WASM_VECTOR_LEN = array.length;
|
|
3629
|
+
return ptr;
|
|
3630
|
+
}
|
|
3631
|
+
function passStringToWasm0(arg, malloc, realloc) {
|
|
3632
|
+
if (realloc === void 0) {
|
|
3633
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
3634
|
+
const ptr2 = malloc(buf.length, 1) >>> 0;
|
|
3635
|
+
getUint8ArrayMemory0().subarray(ptr2, ptr2 + buf.length).set(buf);
|
|
3636
|
+
WASM_VECTOR_LEN = buf.length;
|
|
3637
|
+
return ptr2;
|
|
3638
|
+
}
|
|
3639
|
+
let len = arg.length;
|
|
3640
|
+
let ptr = malloc(len, 1) >>> 0;
|
|
3641
|
+
const mem = getUint8ArrayMemory0();
|
|
3642
|
+
let offset = 0;
|
|
3643
|
+
for (; offset < len; offset++) {
|
|
3644
|
+
const code = arg.charCodeAt(offset);
|
|
3645
|
+
if (code > 127) break;
|
|
3646
|
+
mem[ptr + offset] = code;
|
|
3647
|
+
}
|
|
3648
|
+
if (offset !== len) {
|
|
3649
|
+
if (offset !== 0) {
|
|
3650
|
+
arg = arg.slice(offset);
|
|
3651
|
+
}
|
|
3652
|
+
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
|
3653
|
+
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
|
3654
|
+
const ret = cachedTextEncoder.encodeInto(arg, view);
|
|
3655
|
+
offset += ret.written;
|
|
3656
|
+
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
|
3657
|
+
}
|
|
3658
|
+
WASM_VECTOR_LEN = offset;
|
|
3659
|
+
return ptr;
|
|
3660
|
+
}
|
|
3661
|
+
function takeFromExternrefTable0(idx) {
|
|
3662
|
+
const value = wasm.__wbindgen_externrefs.get(idx);
|
|
3663
|
+
wasm.__externref_table_dealloc(idx);
|
|
3664
|
+
return value;
|
|
3665
|
+
}
|
|
3666
|
+
var cachedTextDecoder = new TextDecoder("utf-8", { ignoreBOM: true, fatal: true });
|
|
3667
|
+
cachedTextDecoder.decode();
|
|
3668
|
+
var MAX_SAFARI_DECODE_BYTES = 2146435072;
|
|
3669
|
+
var numBytesDecoded = 0;
|
|
3670
|
+
function decodeText(ptr, len) {
|
|
3671
|
+
numBytesDecoded += len;
|
|
3672
|
+
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
|
|
3673
|
+
cachedTextDecoder = new TextDecoder("utf-8", { ignoreBOM: true, fatal: true });
|
|
3674
|
+
cachedTextDecoder.decode();
|
|
3675
|
+
numBytesDecoded = len;
|
|
3676
|
+
}
|
|
3677
|
+
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
|
3678
|
+
}
|
|
3679
|
+
var cachedTextEncoder = new TextEncoder();
|
|
3680
|
+
if (!("encodeInto" in cachedTextEncoder)) {
|
|
3681
|
+
cachedTextEncoder.encodeInto = function(arg, view) {
|
|
3682
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
3683
|
+
view.set(buf);
|
|
3684
|
+
return {
|
|
3685
|
+
read: arg.length,
|
|
3686
|
+
written: buf.length
|
|
3687
|
+
};
|
|
3688
|
+
};
|
|
3689
|
+
}
|
|
3690
|
+
var WASM_VECTOR_LEN = 0;
|
|
3691
|
+
var wasm;
|
|
3692
|
+
function __wbg_set_wasm(val) {
|
|
3693
|
+
wasm = val;
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
// src/messaging/wasm/pkg/snippets/mls-rs-core-f99cdecbb456b09c/inline0.js
|
|
3697
|
+
var inline0_exports = {};
|
|
3698
|
+
__export(inline0_exports, {
|
|
3699
|
+
date_now: () => date_now
|
|
3700
|
+
});
|
|
3701
|
+
function date_now() {
|
|
3702
|
+
return Date.now();
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
// src/messaging/wasm/loader.ts
|
|
3706
|
+
var import_meta = {};
|
|
3707
|
+
var GLUE_MODULE = "./palbe_mls_bg.js";
|
|
3708
|
+
var SNIPPET_MODULE = "./snippets/mls-rs-core-f99cdecbb456b09c/inline0.js";
|
|
3709
|
+
var inflight = null;
|
|
3710
|
+
var ready = null;
|
|
3711
|
+
async function loadWasmModule() {
|
|
3712
|
+
const wasmUrl = new URL("./pkg/palbe_mls_bg.wasm", import_meta.url);
|
|
3713
|
+
if (wasmUrl.protocol === "file:") {
|
|
3714
|
+
const fsSpecifier = ["node", "fs/promises"].join(":");
|
|
3715
|
+
const { readFile } = await import(
|
|
3716
|
+
/* webpackIgnore: true */
|
|
3717
|
+
fsSpecifier
|
|
3718
|
+
);
|
|
3719
|
+
const bytes = await readFile(wasmUrl);
|
|
3720
|
+
return WebAssembly.compile(bytes);
|
|
3721
|
+
}
|
|
3722
|
+
const response = await fetch(wasmUrl);
|
|
3723
|
+
if (typeof WebAssembly.compileStreaming === "function") {
|
|
3724
|
+
try {
|
|
3725
|
+
return await WebAssembly.compileStreaming(Promise.resolve(response));
|
|
3726
|
+
} catch {
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
const buffer = await response.arrayBuffer();
|
|
3730
|
+
return WebAssembly.compile(buffer);
|
|
3731
|
+
}
|
|
3732
|
+
function buildImports() {
|
|
3733
|
+
return {
|
|
3734
|
+
[GLUE_MODULE]: palbe_mls_bg_exports,
|
|
3735
|
+
[SNIPPET_MODULE]: inline0_exports
|
|
3736
|
+
};
|
|
3737
|
+
}
|
|
3738
|
+
function initMls() {
|
|
3739
|
+
if (ready) return Promise.resolve(ready);
|
|
3740
|
+
if (inflight) return inflight;
|
|
3741
|
+
inflight = (async () => {
|
|
3742
|
+
const module2 = await loadWasmModule();
|
|
3743
|
+
const instance = await WebAssembly.instantiate(module2, buildImports());
|
|
3744
|
+
const setWasm = __wbg_set_wasm;
|
|
3745
|
+
setWasm(instance.exports);
|
|
3746
|
+
instance.exports.__wbindgen_start?.();
|
|
3747
|
+
const g = {
|
|
3748
|
+
generateClient,
|
|
3749
|
+
setPanicHook,
|
|
3750
|
+
MlsClient,
|
|
3751
|
+
MlsGroup
|
|
3752
|
+
};
|
|
3753
|
+
try {
|
|
3754
|
+
g.setPanicHook();
|
|
3755
|
+
} catch {
|
|
3756
|
+
}
|
|
3757
|
+
ready = g;
|
|
3758
|
+
return g;
|
|
3759
|
+
})();
|
|
3760
|
+
return inflight;
|
|
3761
|
+
}
|
|
3762
|
+
function requireMls() {
|
|
3763
|
+
if (!ready) {
|
|
3764
|
+
throw new Error("palbe-mls WASM not initialized \u2014 await initMls() first");
|
|
3765
|
+
}
|
|
3766
|
+
return ready;
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
// src/messaging/wasm/bridge.ts
|
|
3770
|
+
function toBytes(v) {
|
|
3771
|
+
if (v instanceof Uint8Array) return v;
|
|
3772
|
+
if (Array.isArray(v)) return Uint8Array.from(v);
|
|
3773
|
+
if (v == null) return new Uint8Array(0);
|
|
3774
|
+
if (ArrayBuffer.isView(v)) return new Uint8Array(v.buffer);
|
|
3775
|
+
throw new Error("expected byte field (Uint8Array | number[])");
|
|
3776
|
+
}
|
|
3777
|
+
function optBytes(v) {
|
|
3778
|
+
if (v == null) return void 0;
|
|
3779
|
+
return toBytes(v);
|
|
3780
|
+
}
|
|
3781
|
+
var MlsGroupHandle = class {
|
|
3782
|
+
/** @internal */
|
|
3783
|
+
constructor(raw) {
|
|
3784
|
+
this.raw = raw;
|
|
3785
|
+
}
|
|
3786
|
+
raw;
|
|
3787
|
+
/** The MLS rfc_group_id (routing key / storage key) bytes. */
|
|
3788
|
+
groupId() {
|
|
3789
|
+
return this.raw.groupId();
|
|
3790
|
+
}
|
|
3791
|
+
/** Current applied epoch (BigInt → number; epochs stay well under 2^53). */
|
|
3792
|
+
currentEpoch() {
|
|
3793
|
+
return Number(this.raw.currentEpoch());
|
|
3794
|
+
}
|
|
3795
|
+
/** Stage a member add (PENDING after). */
|
|
3796
|
+
addMember(keyPackageMsg) {
|
|
3797
|
+
return normalizeAddResult(this.raw.addMember(keyPackageMsg));
|
|
3798
|
+
}
|
|
3799
|
+
/** Stage a member remove by identity bytes (PENDING after). */
|
|
3800
|
+
removeMember(memberId) {
|
|
3801
|
+
const r = this.raw.removeMember(memberId);
|
|
3802
|
+
return { commit: toBytes(r.commit) };
|
|
3803
|
+
}
|
|
3804
|
+
/** Stage a SINGLE atomic add+remove commit (PENDING after). */
|
|
3805
|
+
commitChanges(addKeyPackages, removeMemberIds) {
|
|
3806
|
+
return normalizeAddResult(this.raw.commitChanges(addKeyPackages, removeMemberIds));
|
|
3807
|
+
}
|
|
3808
|
+
/** Apply the staged commit (advance the local epoch). Only after server-accept. */
|
|
3809
|
+
applyPendingCommit() {
|
|
3810
|
+
this.raw.applyPendingCommit();
|
|
3811
|
+
}
|
|
3812
|
+
/** Discard the staged commit without advancing (the 409-rebase path). */
|
|
3813
|
+
clearPendingCommit() {
|
|
3814
|
+
this.raw.clearPendingCommit();
|
|
3815
|
+
}
|
|
3816
|
+
/** True while a staged (unapplied) commit exists. */
|
|
3817
|
+
hasPendingCommit() {
|
|
3818
|
+
return this.raw.hasPendingCommit();
|
|
3819
|
+
}
|
|
3820
|
+
/** Encrypt an application message at the current epoch → PrivateMessage bytes. */
|
|
3821
|
+
encryptApplicationMessage(plaintext) {
|
|
3822
|
+
return this.raw.encryptApplicationMessage(plaintext);
|
|
3823
|
+
}
|
|
3824
|
+
/** Process an incoming MLSMessage and apply its effect (normalized result). */
|
|
3825
|
+
processIncomingMessage(message) {
|
|
3826
|
+
return normalizeReceived(this.raw.processIncomingMessage(message));
|
|
3827
|
+
}
|
|
3828
|
+
/** Inspect an incoming Commit WITHOUT applying it (the gate's crypto-truth). */
|
|
3829
|
+
describeIncomingCommit(commitMsg) {
|
|
3830
|
+
const d = this.raw.describeIncomingCommit(commitMsg);
|
|
3831
|
+
return {
|
|
3832
|
+
kind: d.kind,
|
|
3833
|
+
addedIdentities: d.addedIdentities.map(toBytes),
|
|
3834
|
+
removedIdentities: d.removedIdentities.map(toBytes)
|
|
3835
|
+
};
|
|
3836
|
+
}
|
|
3837
|
+
/** Persist the current group state through the JS group-storage callback. */
|
|
3838
|
+
writeToStorage() {
|
|
3839
|
+
this.raw.writeToStorage();
|
|
3840
|
+
}
|
|
3841
|
+
};
|
|
3842
|
+
function normalizeAddResult(v) {
|
|
3843
|
+
const r = v;
|
|
3844
|
+
return {
|
|
3845
|
+
commit: toBytes(r.commit),
|
|
3846
|
+
welcome: optBytes(r.welcome),
|
|
3847
|
+
addedLeafIndices: (r.addedLeafIndices ?? []).map((n) => Number(n))
|
|
3848
|
+
};
|
|
3849
|
+
}
|
|
3850
|
+
function normalizeReceived(v) {
|
|
3851
|
+
if (v === "Proposal") return { type: "proposal" };
|
|
3852
|
+
if (v === "Other") return { type: "other" };
|
|
3853
|
+
const obj = v;
|
|
3854
|
+
if (obj.Application) {
|
|
3855
|
+
const a = obj.Application;
|
|
3856
|
+
return { type: "application", sender: toBytes(a.sender), data: toBytes(a.data) };
|
|
3857
|
+
}
|
|
3858
|
+
if (obj.CommitApplied) {
|
|
3859
|
+
const c = obj.CommitApplied;
|
|
3860
|
+
return { type: "commitApplied", epoch: Number(c.epoch), removedSelf: Boolean(c.removedSelf) };
|
|
3861
|
+
}
|
|
3862
|
+
return { type: "other" };
|
|
3863
|
+
}
|
|
3864
|
+
var MlsClientHandle = class {
|
|
3865
|
+
/** @internal */
|
|
3866
|
+
constructor(raw) {
|
|
3867
|
+
this.raw = raw;
|
|
3868
|
+
}
|
|
3869
|
+
raw;
|
|
3870
|
+
/** The SP-0 keydir enroll material (`{ signatureKey, credential, capabilities }`). */
|
|
3871
|
+
enrollmentMaterial() {
|
|
3872
|
+
const m = this.raw.enrollmentMaterial();
|
|
3873
|
+
return {
|
|
3874
|
+
signatureKey: toBytes(m.signatureKey),
|
|
3875
|
+
credential: toBytes(m.credential),
|
|
3876
|
+
capabilities: toBytes(m.capabilities)
|
|
3877
|
+
};
|
|
3878
|
+
}
|
|
3879
|
+
/** The device's STABLE signature PUBLIC key bytes (the keydir `signature_key`). */
|
|
3880
|
+
signaturePublicKey() {
|
|
3881
|
+
return this.raw.signaturePublicKey();
|
|
3882
|
+
}
|
|
3883
|
+
/** The MLS Credential wire bytes (the keydir `credential`). */
|
|
3884
|
+
credential() {
|
|
3885
|
+
return this.raw.credential();
|
|
3886
|
+
}
|
|
3887
|
+
/** The MLS Capabilities wire bytes (the keydir `capabilities`). */
|
|
3888
|
+
capabilities() {
|
|
3889
|
+
return this.raw.capabilities();
|
|
3890
|
+
}
|
|
3891
|
+
/** The serialized STABLE signature keypair (secret-bearing — persist sealed). */
|
|
3892
|
+
signatureKeypair() {
|
|
3893
|
+
return this.raw.signatureKeypair();
|
|
3894
|
+
}
|
|
3895
|
+
/** Mint a one-use KeyPackage (RAW RFC 9420 §10 bytes). */
|
|
3896
|
+
generateKeyPackage() {
|
|
3897
|
+
return this.raw.generateKeyPackage();
|
|
3898
|
+
}
|
|
3899
|
+
/** Create a new group at epoch 0 (engine mints the rfc_group_id). */
|
|
3900
|
+
createGroup(groupId) {
|
|
3901
|
+
return new MlsGroupHandle(this.raw.createGroup(groupId ?? void 0));
|
|
3902
|
+
}
|
|
3903
|
+
/** Join a group from a Welcome (opaque MLSMessage bytes). */
|
|
3904
|
+
joinGroup(welcome) {
|
|
3905
|
+
return new MlsGroupHandle(this.raw.joinGroup(welcome));
|
|
3906
|
+
}
|
|
3907
|
+
/** Re-hydrate an already-joined group by its rfc_group_id bytes. */
|
|
3908
|
+
loadGroup(groupId) {
|
|
3909
|
+
return new MlsGroupHandle(this.raw.loadGroup(groupId));
|
|
3910
|
+
}
|
|
3911
|
+
};
|
|
3912
|
+
function generateClient2(id, groupStorage, keyPackageStorage, signatureKeypair) {
|
|
3913
|
+
const { generateClient: gen } = requireMls();
|
|
3914
|
+
return new MlsClientHandle(
|
|
3915
|
+
gen(id, groupStorage, keyPackageStorage, signatureKeypair ?? void 0)
|
|
3916
|
+
);
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3919
|
+
// src/messaging/mls-engine.ts
|
|
3920
|
+
var utf8 = new TextEncoder();
|
|
3921
|
+
var gidHex = bytesToHex;
|
|
3922
|
+
var MlsEngine = class _MlsEngine {
|
|
3923
|
+
constructor(userId, deviceId, client, groupStore, kpStore) {
|
|
3924
|
+
this.userId = userId;
|
|
3925
|
+
this.deviceId = deviceId;
|
|
3926
|
+
this.client = client;
|
|
3927
|
+
this.groupStore = groupStore;
|
|
3928
|
+
this.kpStore = kpStore;
|
|
3929
|
+
}
|
|
3930
|
+
userId;
|
|
3931
|
+
deviceId;
|
|
3932
|
+
client;
|
|
3933
|
+
groupStore;
|
|
3934
|
+
kpStore;
|
|
3935
|
+
groups = /* @__PURE__ */ new Map();
|
|
3936
|
+
chain = Promise.resolve();
|
|
3937
|
+
/**
|
|
3938
|
+
* Build the engine: init the WASM, hydrate the durable stores into the sync
|
|
3939
|
+
* caches, generate (or restore) the device client. Idempotent restore: the
|
|
3940
|
+
* persisted signature keypair keeps the SAME MLS signing identity across
|
|
3941
|
+
* reloads; the group-state cache makes already-joined groups reloadable.
|
|
3942
|
+
*/
|
|
3943
|
+
static async create(opts) {
|
|
3944
|
+
await initMls();
|
|
3945
|
+
await Promise.all([opts.groupStore.hydrate(), opts.kpStore.hydrate()]);
|
|
3946
|
+
const persistedKeypair = await opts.sigStore.load(opts.userId);
|
|
3947
|
+
const idBytes = utf8.encode(opts.deviceId);
|
|
3948
|
+
const client = generateClient2(idBytes, opts.groupStore, opts.kpStore, persistedKeypair);
|
|
3949
|
+
if (!persistedKeypair) {
|
|
3950
|
+
await opts.sigStore.save(opts.userId, client.signatureKeypair());
|
|
3951
|
+
}
|
|
3952
|
+
return new _MlsEngine(opts.userId, opts.deviceId, client, opts.groupStore, opts.kpStore);
|
|
3953
|
+
}
|
|
3954
|
+
/** Serialize an engine op onto the chain so the critical section is exclusive. */
|
|
3955
|
+
enqueue(fn) {
|
|
3956
|
+
const run = this.chain.then(fn, fn);
|
|
3957
|
+
this.chain = run.then(
|
|
3958
|
+
() => void 0,
|
|
3959
|
+
() => void 0
|
|
3960
|
+
);
|
|
3961
|
+
return run;
|
|
3962
|
+
}
|
|
3963
|
+
/** Flush both storage caches durably (the post-mutation durability boundary). */
|
|
3964
|
+
async flush() {
|
|
3965
|
+
await Promise.all([this.groupStore.flush(), this.kpStore.flush()]);
|
|
3966
|
+
}
|
|
3967
|
+
// ── Enroll material / key packages ──
|
|
3968
|
+
enrollmentMaterial() {
|
|
3969
|
+
return this.enqueue(() => this.client.enrollmentMaterial());
|
|
3970
|
+
}
|
|
3971
|
+
signaturePublicKey() {
|
|
3972
|
+
return this.enqueue(() => this.client.signaturePublicKey());
|
|
3973
|
+
}
|
|
3974
|
+
generateKeyPackage() {
|
|
3975
|
+
return this.enqueue(async () => {
|
|
3976
|
+
const kp = this.client.generateKeyPackage();
|
|
3977
|
+
await this.flush();
|
|
3978
|
+
return kp;
|
|
3979
|
+
});
|
|
3980
|
+
}
|
|
3981
|
+
// ── Group create / join / reload ──
|
|
3982
|
+
/** Create a group; returns its rfc_group_id (the routing key). */
|
|
3983
|
+
createGroup() {
|
|
3984
|
+
return this.enqueue(async () => {
|
|
3985
|
+
const group = this.client.createGroup();
|
|
3986
|
+
const gid = group.groupId();
|
|
3987
|
+
this.groups.set(gidHex(gid), group);
|
|
3988
|
+
group.writeToStorage();
|
|
3989
|
+
await this.flush();
|
|
3990
|
+
return gid;
|
|
3991
|
+
});
|
|
3992
|
+
}
|
|
3993
|
+
/** Join from a Welcome; returns the joined group's rfc_group_id. */
|
|
3994
|
+
joinFromWelcome(welcome) {
|
|
3995
|
+
return this.enqueue(async () => {
|
|
3996
|
+
const group = this.client.joinGroup(welcome);
|
|
3997
|
+
const gid = group.groupId();
|
|
3998
|
+
this.groups.set(gidHex(gid), group);
|
|
3999
|
+
group.writeToStorage();
|
|
4000
|
+
await this.flush();
|
|
4001
|
+
return gid;
|
|
4002
|
+
});
|
|
4003
|
+
}
|
|
4004
|
+
/** The current applied epoch for a group (0 if unknown). */
|
|
4005
|
+
epoch(rfcGroupId) {
|
|
4006
|
+
return this.enqueue(() => {
|
|
4007
|
+
const g = this.handle(rfcGroupId);
|
|
4008
|
+
return g ? g.currentEpoch() : 0;
|
|
4009
|
+
});
|
|
4010
|
+
}
|
|
4011
|
+
// ── Membership commits (staged → applied/cleared by the rebase loop) ──
|
|
4012
|
+
/** Stage a SINGLE atomic add+remove commit. Returns the AddResult (PENDING). */
|
|
4013
|
+
commitChanges(rfcGroupId, adds, removeMemberIds) {
|
|
4014
|
+
return this.enqueue(() => {
|
|
4015
|
+
const g = this.requireHandle(rfcGroupId);
|
|
4016
|
+
return g.commitChanges(adds, removeMemberIds);
|
|
4017
|
+
});
|
|
4018
|
+
}
|
|
4019
|
+
/** Apply the staged commit (advance the epoch). Only after a server-accept. */
|
|
4020
|
+
applyPendingCommit(rfcGroupId) {
|
|
4021
|
+
return this.enqueue(async () => {
|
|
4022
|
+
const g = this.requireHandle(rfcGroupId);
|
|
4023
|
+
g.applyPendingCommit();
|
|
4024
|
+
g.writeToStorage();
|
|
4025
|
+
await this.flush();
|
|
4026
|
+
});
|
|
4027
|
+
}
|
|
4028
|
+
/** Discard the staged commit (the 409-rebase path). */
|
|
4029
|
+
clearPendingCommit(rfcGroupId) {
|
|
4030
|
+
return this.enqueue(() => {
|
|
4031
|
+
const g = this.handle(rfcGroupId);
|
|
4032
|
+
if (g?.hasPendingCommit()) g.clearPendingCommit();
|
|
4033
|
+
});
|
|
4034
|
+
}
|
|
4035
|
+
// ── Application messages ──
|
|
4036
|
+
/** Encrypt a plaintext at the current epoch → PrivateMessage bytes. */
|
|
4037
|
+
encryptApplication(rfcGroupId, plaintext) {
|
|
4038
|
+
return this.enqueue(async () => {
|
|
4039
|
+
const g = this.requireHandle(rfcGroupId);
|
|
4040
|
+
const ct = g.encryptApplicationMessage(plaintext);
|
|
4041
|
+
g.writeToStorage();
|
|
4042
|
+
await this.flush();
|
|
4043
|
+
return ct;
|
|
4044
|
+
});
|
|
4045
|
+
}
|
|
4046
|
+
/**
|
|
4047
|
+
* Process an incoming MLSMessage (decrypt application / apply commit). Returns
|
|
4048
|
+
* the normalized ReceivedMessage. `describeIncomingCommit` could gate a commit
|
|
4049
|
+
* before applying when the server fans a declaration; today the live server
|
|
4050
|
+
* does not, so we process directly (parity with the iOS nil-declaration path).
|
|
4051
|
+
*/
|
|
4052
|
+
processIncoming(rfcGroupId, blob) {
|
|
4053
|
+
return this.enqueue(async () => {
|
|
4054
|
+
const g = this.requireHandle(rfcGroupId);
|
|
4055
|
+
const received = g.processIncomingMessage(blob);
|
|
4056
|
+
g.writeToStorage();
|
|
4057
|
+
await this.flush();
|
|
4058
|
+
return received;
|
|
4059
|
+
});
|
|
4060
|
+
}
|
|
4061
|
+
/** Inspect a commit WITHOUT applying (the reconciliation gate's crypto-truth). */
|
|
4062
|
+
describeIncomingCommit(rfcGroupId, commit) {
|
|
4063
|
+
return this.enqueue(() => this.requireHandle(rfcGroupId).describeIncomingCommit(commit));
|
|
4064
|
+
}
|
|
4065
|
+
/** Persist the current group state (explicit checkpoint). */
|
|
4066
|
+
writeToStorage(rfcGroupId) {
|
|
4067
|
+
return this.enqueue(async () => {
|
|
4068
|
+
const g = this.handle(rfcGroupId);
|
|
4069
|
+
if (g) {
|
|
4070
|
+
g.writeToStorage();
|
|
4071
|
+
await this.flush();
|
|
4072
|
+
}
|
|
4073
|
+
});
|
|
4074
|
+
}
|
|
4075
|
+
// ── Handle resolution ──
|
|
4076
|
+
/** Resolve (or reload from storage) the handle for a group, or undefined. */
|
|
4077
|
+
handle(rfcGroupId) {
|
|
4078
|
+
const key = gidHex(rfcGroupId);
|
|
4079
|
+
const cached = this.groups.get(key);
|
|
4080
|
+
if (cached) return cached;
|
|
4081
|
+
try {
|
|
4082
|
+
const g = this.client.loadGroup(rfcGroupId);
|
|
4083
|
+
this.groups.set(key, g);
|
|
4084
|
+
return g;
|
|
4085
|
+
} catch {
|
|
4086
|
+
return void 0;
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
requireHandle(rfcGroupId) {
|
|
4090
|
+
const g = this.handle(rfcGroupId);
|
|
4091
|
+
if (!g) throw new Error(`MLS group not loaded: ${gidHex(rfcGroupId)}`);
|
|
4092
|
+
return g;
|
|
4093
|
+
}
|
|
4094
|
+
};
|
|
4095
|
+
|
|
4096
|
+
// src/messaging/registry.ts
|
|
4097
|
+
var GroupRegistry = class {
|
|
4098
|
+
byRfc = /* @__PURE__ */ new Map();
|
|
4099
|
+
catalog = null;
|
|
4100
|
+
chatFactory = null;
|
|
4101
|
+
onListChange = null;
|
|
4102
|
+
/** Wire the observable chat-list sink + the pointer-stable Chat factory, then
|
|
4103
|
+
* replay the current set so a sink attached after groups loaded reflects them. */
|
|
4104
|
+
attachChatList(onListChange, factory) {
|
|
4105
|
+
this.onListChange = onListChange;
|
|
4106
|
+
this.chatFactory = factory;
|
|
4107
|
+
this.publish();
|
|
4108
|
+
}
|
|
4109
|
+
/** Hydrate from the durable catalog (live entries win — fresher epoch). */
|
|
4110
|
+
async attachCatalog(catalog) {
|
|
4111
|
+
this.catalog = catalog;
|
|
4112
|
+
let changed = false;
|
|
4113
|
+
for (const c of await catalog.all()) {
|
|
4114
|
+
if (!this.byRfc.has(c.rfcGroupId)) {
|
|
4115
|
+
this.byRfc.set(c.rfcGroupId, {
|
|
4116
|
+
displayId: c.displayId,
|
|
4117
|
+
rfcGroupId: c.rfcGroupId,
|
|
4118
|
+
currentEpoch: c.currentEpoch,
|
|
4119
|
+
ownerUserId: c.ownerUserId,
|
|
4120
|
+
directKey: c.directKey,
|
|
4121
|
+
name: c.name
|
|
4122
|
+
});
|
|
4123
|
+
changed = true;
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
if (changed) this.publish();
|
|
4127
|
+
}
|
|
4128
|
+
register(group) {
|
|
4129
|
+
const isNew = !this.byRfc.has(group.rfcGroupId);
|
|
4130
|
+
this.byRfc.set(group.rfcGroupId, group);
|
|
4131
|
+
void this.persist(group);
|
|
4132
|
+
if (isNew) this.publish();
|
|
4133
|
+
}
|
|
4134
|
+
remove(rfcGroupId) {
|
|
4135
|
+
if (!this.byRfc.has(rfcGroupId)) return;
|
|
4136
|
+
this.byRfc.delete(rfcGroupId);
|
|
4137
|
+
if (this.catalog) void this.catalog.remove(rfcGroupId);
|
|
4138
|
+
this.publish();
|
|
4139
|
+
}
|
|
4140
|
+
group(rfcGroupId) {
|
|
4141
|
+
return this.byRfc.get(rfcGroupId);
|
|
4142
|
+
}
|
|
4143
|
+
/** The single known group, if exactly one (the wire-gap routing fallback). */
|
|
4144
|
+
soleGroup() {
|
|
4145
|
+
return this.byRfc.size === 1 ? this.byRfc.values().next().value : void 0;
|
|
4146
|
+
}
|
|
4147
|
+
all() {
|
|
4148
|
+
return [...this.byRfc.values()];
|
|
4149
|
+
}
|
|
4150
|
+
/** Advance a cached epoch (after a processed commit). Does NOT publish (the
|
|
4151
|
+
* group SET is unchanged — only metadata). */
|
|
4152
|
+
bumpEpoch(rfcGroupId, epoch) {
|
|
4153
|
+
const g = this.byRfc.get(rfcGroupId);
|
|
4154
|
+
if (!g) return;
|
|
4155
|
+
const bumped = { ...g, currentEpoch: epoch };
|
|
4156
|
+
this.byRfc.set(rfcGroupId, bumped);
|
|
4157
|
+
void this.persist(bumped);
|
|
4158
|
+
}
|
|
4159
|
+
publish() {
|
|
4160
|
+
const factory = this.chatFactory;
|
|
4161
|
+
if (!this.onListChange || !factory) return;
|
|
4162
|
+
const chats = this.all().map((g) => factory(g));
|
|
4163
|
+
this.onListChange(chats);
|
|
4164
|
+
}
|
|
4165
|
+
async persist(group) {
|
|
4166
|
+
if (!this.catalog) return;
|
|
4167
|
+
try {
|
|
4168
|
+
await this.catalog.upsert({
|
|
4169
|
+
displayId: group.displayId,
|
|
4170
|
+
rfcGroupId: group.rfcGroupId,
|
|
4171
|
+
currentEpoch: group.currentEpoch,
|
|
4172
|
+
ownerUserId: group.ownerUserId,
|
|
4173
|
+
directKey: group.directKey,
|
|
4174
|
+
name: group.name
|
|
4175
|
+
});
|
|
4176
|
+
} catch {
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
};
|
|
4180
|
+
|
|
4181
|
+
// src/messaging/storage.ts
|
|
4182
|
+
var hasIndexedDB = () => typeof indexedDB !== "undefined" && typeof crypto !== "undefined" && !!crypto.subtle;
|
|
4183
|
+
function copyToArrayBuffer(view) {
|
|
4184
|
+
const out = new ArrayBuffer(view.byteLength);
|
|
4185
|
+
new Uint8Array(out).set(view);
|
|
4186
|
+
return out;
|
|
4187
|
+
}
|
|
4188
|
+
var MemoryKV = class {
|
|
4189
|
+
map = /* @__PURE__ */ new Map();
|
|
4190
|
+
get(key) {
|
|
4191
|
+
return Promise.resolve(this.map.get(key));
|
|
4192
|
+
}
|
|
4193
|
+
set(key, value) {
|
|
4194
|
+
this.map.set(key, value.slice());
|
|
4195
|
+
return Promise.resolve();
|
|
4196
|
+
}
|
|
4197
|
+
delete(key) {
|
|
4198
|
+
this.map.delete(key);
|
|
4199
|
+
return Promise.resolve();
|
|
4200
|
+
}
|
|
4201
|
+
keys(prefix) {
|
|
4202
|
+
return Promise.resolve([...this.map.keys()].filter((k) => k.startsWith(prefix)));
|
|
4203
|
+
}
|
|
4204
|
+
};
|
|
4205
|
+
var DB_NAME = "palbe.messaging";
|
|
4206
|
+
var STORE = "kv";
|
|
4207
|
+
var KEY_STORE = "keys";
|
|
4208
|
+
var SEAL_KEY_ID = "seal-key-v1";
|
|
4209
|
+
function openDb() {
|
|
4210
|
+
return new Promise((resolve, reject) => {
|
|
4211
|
+
const req = indexedDB.open(DB_NAME, 1);
|
|
4212
|
+
req.onupgradeneeded = () => {
|
|
4213
|
+
const db = req.result;
|
|
4214
|
+
if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE);
|
|
4215
|
+
if (!db.objectStoreNames.contains(KEY_STORE)) db.createObjectStore(KEY_STORE);
|
|
4216
|
+
};
|
|
4217
|
+
req.onsuccess = () => resolve(req.result);
|
|
4218
|
+
req.onerror = () => reject(req.error);
|
|
4219
|
+
});
|
|
4220
|
+
}
|
|
4221
|
+
function idbReq(req) {
|
|
4222
|
+
return new Promise((resolve, reject) => {
|
|
4223
|
+
req.onsuccess = () => resolve(req.result);
|
|
4224
|
+
req.onerror = () => reject(req.error);
|
|
4225
|
+
});
|
|
4226
|
+
}
|
|
4227
|
+
async function getSealKey(db) {
|
|
4228
|
+
const existing = await idbReq(
|
|
4229
|
+
db.transaction(KEY_STORE, "readonly").objectStore(KEY_STORE).get(SEAL_KEY_ID)
|
|
4230
|
+
);
|
|
4231
|
+
if (existing) return existing;
|
|
4232
|
+
const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, [
|
|
4233
|
+
"encrypt",
|
|
4234
|
+
"decrypt"
|
|
4235
|
+
]);
|
|
4236
|
+
await idbReq(db.transaction(KEY_STORE, "readwrite").objectStore(KEY_STORE).put(key, SEAL_KEY_ID));
|
|
4237
|
+
return key;
|
|
4238
|
+
}
|
|
4239
|
+
var IndexedDbKV = class {
|
|
4240
|
+
dbPromise = null;
|
|
4241
|
+
keyPromise = null;
|
|
4242
|
+
db() {
|
|
4243
|
+
if (!this.dbPromise) this.dbPromise = openDb();
|
|
4244
|
+
return this.dbPromise;
|
|
4245
|
+
}
|
|
4246
|
+
async sealKey() {
|
|
4247
|
+
if (!this.keyPromise) this.keyPromise = this.db().then(getSealKey);
|
|
4248
|
+
return this.keyPromise;
|
|
4249
|
+
}
|
|
4250
|
+
async seal(plain) {
|
|
4251
|
+
const key = await this.sealKey();
|
|
4252
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
4253
|
+
const ct = new Uint8Array(
|
|
4254
|
+
await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, copyToArrayBuffer(plain))
|
|
4255
|
+
);
|
|
4256
|
+
const out = new Uint8Array(iv.length + ct.length);
|
|
4257
|
+
out.set(iv, 0);
|
|
4258
|
+
out.set(ct, iv.length);
|
|
4259
|
+
return out;
|
|
4260
|
+
}
|
|
4261
|
+
async open(sealed) {
|
|
4262
|
+
const key = await this.sealKey();
|
|
4263
|
+
const iv = copyToArrayBuffer(sealed.subarray(0, 12));
|
|
4264
|
+
const ct = copyToArrayBuffer(sealed.subarray(12));
|
|
4265
|
+
const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
|
|
4266
|
+
return new Uint8Array(pt);
|
|
4267
|
+
}
|
|
4268
|
+
async get(k) {
|
|
4269
|
+
const db = await this.db();
|
|
4270
|
+
const sealed = await idbReq(db.transaction(STORE, "readonly").objectStore(STORE).get(k));
|
|
4271
|
+
if (!sealed) return void 0;
|
|
4272
|
+
return this.open(sealed);
|
|
4273
|
+
}
|
|
4274
|
+
async set(k, v) {
|
|
4275
|
+
const db = await this.db();
|
|
4276
|
+
const sealed = await this.seal(v);
|
|
4277
|
+
await idbReq(db.transaction(STORE, "readwrite").objectStore(STORE).put(sealed, k));
|
|
4278
|
+
}
|
|
4279
|
+
async delete(k) {
|
|
4280
|
+
const db = await this.db();
|
|
4281
|
+
await idbReq(db.transaction(STORE, "readwrite").objectStore(STORE).delete(k));
|
|
4282
|
+
}
|
|
4283
|
+
async keys(prefix) {
|
|
4284
|
+
const db = await this.db();
|
|
4285
|
+
const all = await idbReq(
|
|
4286
|
+
db.transaction(STORE, "readonly").objectStore(STORE).getAllKeys()
|
|
4287
|
+
);
|
|
4288
|
+
return all.map(String).filter((k) => k.startsWith(prefix));
|
|
4289
|
+
}
|
|
4290
|
+
};
|
|
4291
|
+
function createDurableKV() {
|
|
4292
|
+
return hasIndexedDB() ? new IndexedDbKV() : new MemoryKV();
|
|
4293
|
+
}
|
|
4294
|
+
function bytesKey(prefix, id) {
|
|
4295
|
+
return prefix + bytesToHex(id);
|
|
1187
4296
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
4297
|
+
var GroupStateStorage = class {
|
|
4298
|
+
constructor(kv) {
|
|
4299
|
+
this.kv = kv;
|
|
4300
|
+
}
|
|
4301
|
+
kv;
|
|
4302
|
+
// In-memory authoritative cache (the engine reads/writes this synchronously).
|
|
4303
|
+
stateCache = /* @__PURE__ */ new Map();
|
|
4304
|
+
epochCache = /* @__PURE__ */ new Map();
|
|
4305
|
+
maxEpochCache = /* @__PURE__ */ new Map();
|
|
4306
|
+
// Keys touched since the last flush (durable write queue).
|
|
4307
|
+
dirty = /* @__PURE__ */ new Set();
|
|
4308
|
+
// ── sync engine seam ──
|
|
4309
|
+
state(groupId) {
|
|
4310
|
+
return this.stateCache.get(bytesKey("", groupId));
|
|
4311
|
+
}
|
|
4312
|
+
epoch(groupId, epochId) {
|
|
4313
|
+
return this.epochCache.get(`${bytesKey("", groupId)}:${epochId}`);
|
|
4314
|
+
}
|
|
4315
|
+
write(groupId, groupState, epochInserts, epochUpdates) {
|
|
4316
|
+
const gid = bytesKey("", groupId);
|
|
4317
|
+
this.stateCache.set(gid, groupState.slice());
|
|
4318
|
+
this.dirty.add(`state:${gid}`);
|
|
4319
|
+
let max = this.maxEpochCache.get(gid) ?? -1n;
|
|
4320
|
+
for (const rec of [...epochInserts, ...epochUpdates]) {
|
|
4321
|
+
this.epochCache.set(`${gid}:${rec.id}`, rec.data.slice());
|
|
4322
|
+
this.dirty.add(`epoch:${gid}:${rec.id}`);
|
|
4323
|
+
if (rec.id > max) max = rec.id;
|
|
4324
|
+
}
|
|
4325
|
+
if (max >= 0n) {
|
|
4326
|
+
this.maxEpochCache.set(gid, max);
|
|
4327
|
+
this.dirty.add(`maxEpoch:${gid}`);
|
|
1192
4328
|
}
|
|
1193
|
-
const probe = "__palbase_flags_probe__";
|
|
1194
|
-
localStorage.setItem(probe, "1");
|
|
1195
|
-
localStorage.removeItem(probe);
|
|
1196
|
-
return localStorage;
|
|
1197
|
-
} catch {
|
|
1198
|
-
return null;
|
|
1199
4329
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
if (typeof document === "undefined") {
|
|
1203
|
-
return null;
|
|
4330
|
+
maxEpochId(groupId) {
|
|
4331
|
+
return this.maxEpochCache.get(bytesKey("", groupId));
|
|
1204
4332
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
4333
|
+
// ── async durable flush (called by the engine wrapper after a mutating op) ──
|
|
4334
|
+
async flush() {
|
|
4335
|
+
if (this.dirty.size === 0) return;
|
|
4336
|
+
const pending = [...this.dirty];
|
|
4337
|
+
this.dirty.clear();
|
|
4338
|
+
await Promise.all(
|
|
4339
|
+
pending.map(async (key) => {
|
|
4340
|
+
if (key.startsWith("state:")) {
|
|
4341
|
+
const gid = key.slice("state:".length);
|
|
4342
|
+
const v = this.stateCache.get(gid);
|
|
4343
|
+
if (v) await this.kv.set(`gss:state:${gid}`, v);
|
|
4344
|
+
} else if (key.startsWith("epoch:")) {
|
|
4345
|
+
const rest = key.slice("epoch:".length);
|
|
4346
|
+
const v = this.epochCache.get(rest);
|
|
4347
|
+
if (v) await this.kv.set(`gss:epoch:${rest}`, v);
|
|
4348
|
+
} else if (key.startsWith("maxEpoch:")) {
|
|
4349
|
+
const gid = key.slice("maxEpoch:".length);
|
|
4350
|
+
const max = this.maxEpochCache.get(gid);
|
|
4351
|
+
if (max != null) await this.kv.set(`gss:maxEpoch:${gid}`, encodeBigint(max));
|
|
4352
|
+
}
|
|
4353
|
+
})
|
|
4354
|
+
);
|
|
4355
|
+
}
|
|
4356
|
+
/** Hydrate the in-memory cache from the durable KV (call before generateClient). */
|
|
4357
|
+
async hydrate() {
|
|
4358
|
+
const keys = await this.kv.keys("gss:");
|
|
4359
|
+
for (const k of keys) {
|
|
4360
|
+
const v = await this.kv.get(k);
|
|
4361
|
+
if (!v) continue;
|
|
4362
|
+
if (k.startsWith("gss:state:")) {
|
|
4363
|
+
this.stateCache.set(k.slice("gss:state:".length), v);
|
|
4364
|
+
} else if (k.startsWith("gss:epoch:")) {
|
|
4365
|
+
this.epochCache.set(k.slice("gss:epoch:".length), v);
|
|
4366
|
+
} else if (k.startsWith("gss:maxEpoch:")) {
|
|
4367
|
+
this.maxEpochCache.set(k.slice("gss:maxEpoch:".length), decodeBigint(v));
|
|
4368
|
+
}
|
|
1211
4369
|
}
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
4370
|
+
}
|
|
4371
|
+
async wipe() {
|
|
4372
|
+
this.stateCache.clear();
|
|
4373
|
+
this.epochCache.clear();
|
|
4374
|
+
this.maxEpochCache.clear();
|
|
4375
|
+
this.dirty.clear();
|
|
4376
|
+
for (const k of await this.kv.keys("gss:")) await this.kv.delete(k);
|
|
4377
|
+
}
|
|
4378
|
+
};
|
|
4379
|
+
var KeyPackageStorage = class {
|
|
4380
|
+
constructor(kv) {
|
|
4381
|
+
this.kv = kv;
|
|
4382
|
+
}
|
|
4383
|
+
kv;
|
|
4384
|
+
cache = /* @__PURE__ */ new Map();
|
|
4385
|
+
dirtySet = /* @__PURE__ */ new Set();
|
|
4386
|
+
dirtyDelete = /* @__PURE__ */ new Set();
|
|
4387
|
+
// ── sync engine seam ──
|
|
4388
|
+
delete(id) {
|
|
4389
|
+
const k = bytesKey("", id);
|
|
4390
|
+
this.cache.delete(k);
|
|
4391
|
+
this.dirtyDelete.add(k);
|
|
4392
|
+
this.dirtySet.delete(k);
|
|
4393
|
+
}
|
|
4394
|
+
insert(id, data) {
|
|
4395
|
+
const k = bytesKey("", id);
|
|
4396
|
+
this.cache.set(k, data.slice());
|
|
4397
|
+
this.dirtySet.add(k);
|
|
4398
|
+
this.dirtyDelete.delete(k);
|
|
4399
|
+
}
|
|
4400
|
+
get(id) {
|
|
4401
|
+
return this.cache.get(bytesKey("", id));
|
|
4402
|
+
}
|
|
4403
|
+
// ── async durable flush ──
|
|
4404
|
+
async flush() {
|
|
4405
|
+
if (this.dirtySet.size === 0 && this.dirtyDelete.size === 0) return;
|
|
4406
|
+
const sets = [...this.dirtySet];
|
|
4407
|
+
const dels = [...this.dirtyDelete];
|
|
4408
|
+
this.dirtySet.clear();
|
|
4409
|
+
this.dirtyDelete.clear();
|
|
4410
|
+
await Promise.all([
|
|
4411
|
+
...sets.map((k) => {
|
|
4412
|
+
const v = this.cache.get(k);
|
|
4413
|
+
return v ? this.kv.set(`kps:${k}`, v) : Promise.resolve();
|
|
4414
|
+
}),
|
|
4415
|
+
...dels.map((k) => this.kv.delete(`kps:${k}`))
|
|
4416
|
+
]);
|
|
4417
|
+
}
|
|
4418
|
+
async hydrate() {
|
|
4419
|
+
for (const k of await this.kv.keys("kps:")) {
|
|
4420
|
+
const v = await this.kv.get(k);
|
|
4421
|
+
if (v) this.cache.set(k.slice("kps:".length), v);
|
|
1224
4422
|
}
|
|
1225
|
-
}
|
|
4423
|
+
}
|
|
4424
|
+
async wipe() {
|
|
4425
|
+
this.cache.clear();
|
|
4426
|
+
this.dirtySet.clear();
|
|
4427
|
+
this.dirtyDelete.clear();
|
|
4428
|
+
for (const k of await this.kv.keys("kps:")) await this.kv.delete(k);
|
|
4429
|
+
}
|
|
4430
|
+
};
|
|
4431
|
+
function encodeBigint(n) {
|
|
4432
|
+
return new TextEncoder().encode(n.toString());
|
|
1226
4433
|
}
|
|
1227
|
-
function
|
|
1228
|
-
return
|
|
1229
|
-
now: () => Date.now(),
|
|
1230
|
-
setInterval: () => {
|
|
1231
|
-
throw new Error("palbe flags: polling is disabled server-side");
|
|
1232
|
-
},
|
|
1233
|
-
clearInterval: () => {
|
|
1234
|
-
},
|
|
1235
|
-
storage: null,
|
|
1236
|
-
visibility: null
|
|
1237
|
-
};
|
|
4434
|
+
function decodeBigint(b) {
|
|
4435
|
+
return BigInt(new TextDecoder().decode(b));
|
|
1238
4436
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
4437
|
+
var enc = new TextEncoder();
|
|
4438
|
+
var dec = new TextDecoder();
|
|
4439
|
+
var DeviceIdStore = class {
|
|
4440
|
+
constructor(kv) {
|
|
4441
|
+
this.kv = kv;
|
|
1243
4442
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
4443
|
+
kv;
|
|
4444
|
+
async load(userId) {
|
|
4445
|
+
const v = await this.kv.get(`devid:${userId}`);
|
|
4446
|
+
return v ? dec.decode(v) : void 0;
|
|
4447
|
+
}
|
|
4448
|
+
async save(userId, deviceId) {
|
|
4449
|
+
await this.kv.set(`devid:${userId}`, enc.encode(deviceId));
|
|
4450
|
+
}
|
|
4451
|
+
async clear(userId) {
|
|
4452
|
+
await this.kv.delete(`devid:${userId}`);
|
|
4453
|
+
}
|
|
4454
|
+
};
|
|
4455
|
+
var SignatureKeyStore = class {
|
|
4456
|
+
constructor(kv) {
|
|
4457
|
+
this.kv = kv;
|
|
4458
|
+
}
|
|
4459
|
+
kv;
|
|
4460
|
+
async load(userId) {
|
|
4461
|
+
return this.kv.get(`sigkey:${userId}`);
|
|
4462
|
+
}
|
|
4463
|
+
async save(userId, keypair) {
|
|
4464
|
+
await this.kv.set(`sigkey:${userId}`, keypair);
|
|
4465
|
+
}
|
|
4466
|
+
};
|
|
4467
|
+
|
|
4468
|
+
// src/messaging/coordinator.ts
|
|
4469
|
+
var MessagingCoordinator = class {
|
|
1249
4470
|
constructor(rt) {
|
|
1250
|
-
this.
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
4471
|
+
this.rt = rt;
|
|
4472
|
+
this.kv = createDurableKV();
|
|
4473
|
+
this.deviceStore = new DeviceIdStore(this.kv);
|
|
4474
|
+
this.sigStore = new SignatureKeyStore(this.kv);
|
|
4475
|
+
this.groupStore = new GroupStateStorage(this.kv);
|
|
4476
|
+
this.kpStore = new KeyPackageStorage(this.kv);
|
|
4477
|
+
this.registry.attachChatList(
|
|
4478
|
+
(chats) => {
|
|
4479
|
+
this.chatList = chats;
|
|
4480
|
+
this.emitList();
|
|
4481
|
+
},
|
|
4482
|
+
(group) => this.chatFor(group)
|
|
4483
|
+
);
|
|
4484
|
+
}
|
|
4485
|
+
rt;
|
|
4486
|
+
kv;
|
|
4487
|
+
deviceStore;
|
|
4488
|
+
sigStore;
|
|
4489
|
+
groupStore;
|
|
4490
|
+
kpStore;
|
|
4491
|
+
registry = new GroupRegistry();
|
|
4492
|
+
resolved = null;
|
|
4493
|
+
resolvePromise = null;
|
|
4494
|
+
// Pointer-stable Chat cache (one per rfc id) + the observable chat list.
|
|
4495
|
+
chatByRfc = /* @__PURE__ */ new Map();
|
|
4496
|
+
chatList = [];
|
|
4497
|
+
listListeners = /* @__PURE__ */ new Set();
|
|
4498
|
+
// ── ChatBackend identity ──
|
|
4499
|
+
get selfUserId() {
|
|
4500
|
+
const u = this.rt.auth.currentUser;
|
|
4501
|
+
if (!u) throw new Error("not_signed_in");
|
|
4502
|
+
return u.id;
|
|
4503
|
+
}
|
|
4504
|
+
// ── Public surface (used by the facade) ──
|
|
4505
|
+
/** Self-enroll + start the delivery source (idempotent). The first chat op
|
|
4506
|
+
* calls this implicitly; pb.messaging.start() is the opt-in warm-at-sign-in. */
|
|
4507
|
+
async ensureEnrolled() {
|
|
4508
|
+
await this.resolve();
|
|
4509
|
+
}
|
|
4510
|
+
/** Open the ONE direct chat with userId. INSTANT + LOCAL (no network); the
|
|
4511
|
+
* group materializes on the first send. Pointer-stable per peer pair. */
|
|
4512
|
+
directChat(peerUserId) {
|
|
4513
|
+
const selfId = this.selfUserId;
|
|
4514
|
+
const dk = directKey(selfId, peerUserId);
|
|
4515
|
+
for (const chat2 of this.chatByRfc.values()) {
|
|
4516
|
+
if (chat2.isDirect && chat2.state === "active") {
|
|
4517
|
+
const g = this.registry.all().find((x) => x.displayId === chat2.id);
|
|
4518
|
+
if (g?.directKey === dk) return chat2;
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
const draft = {
|
|
4522
|
+
mode: { kind: "direct", peerUserId },
|
|
4523
|
+
reservedId: `draft:${dk}`
|
|
1257
4524
|
};
|
|
1258
|
-
|
|
1259
|
-
if (
|
|
4525
|
+
const cached = this.chatByRfc.get(draft.reservedId);
|
|
4526
|
+
if (cached) return cached;
|
|
4527
|
+
const chat = new Chat({ draft, kind: "direct", backend: this });
|
|
4528
|
+
this.chatByRfc.set(draft.reservedId, chat);
|
|
4529
|
+
void this.ensureEnrolled().catch(() => {
|
|
4530
|
+
});
|
|
4531
|
+
return chat;
|
|
1260
4532
|
}
|
|
1261
|
-
/**
|
|
1262
|
-
|
|
1263
|
-
|
|
4533
|
+
/** Open a multi-party group chat (LOCAL + LAZY; created on first send). */
|
|
4534
|
+
groupChat(members = []) {
|
|
4535
|
+
const draft = {
|
|
4536
|
+
mode: { kind: "group", members },
|
|
4537
|
+
reservedId: `draft:group:${crypto.randomUUID()}`
|
|
4538
|
+
};
|
|
4539
|
+
const chat = new Chat({ draft, kind: "group", backend: this });
|
|
4540
|
+
this.chatByRfc.set(draft.reservedId, chat);
|
|
4541
|
+
void this.ensureEnrolled().catch(() => {
|
|
4542
|
+
});
|
|
4543
|
+
return chat;
|
|
1264
4544
|
}
|
|
1265
|
-
/**
|
|
1266
|
-
|
|
1267
|
-
|
|
4545
|
+
/** Look up an active chat by id (grp_ display id). */
|
|
4546
|
+
chatById(id) {
|
|
4547
|
+
const g = this.registry.all().find((x) => x.displayId === id);
|
|
4548
|
+
return g ? this.chatFor(g) : this.chatByRfc.get(id) ?? null;
|
|
1268
4549
|
}
|
|
1269
|
-
/**
|
|
1270
|
-
|
|
1271
|
-
|
|
4550
|
+
/** The observable chat list (active chats only). */
|
|
4551
|
+
get chats() {
|
|
4552
|
+
void this.ensureEnrolled().catch(() => {
|
|
4553
|
+
});
|
|
4554
|
+
return this.chatList;
|
|
1272
4555
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
4556
|
+
onChatsChange(cb) {
|
|
4557
|
+
this.listListeners.add(cb);
|
|
4558
|
+
void this.ensureEnrolled().catch(() => {
|
|
4559
|
+
});
|
|
4560
|
+
return () => this.listListeners.delete(cb);
|
|
4561
|
+
}
|
|
4562
|
+
emitList() {
|
|
4563
|
+
for (const cb of this.listListeners) cb();
|
|
4564
|
+
}
|
|
4565
|
+
/** Pointer-stable Chat for a group (one per rfc id). */
|
|
4566
|
+
chatFor(group) {
|
|
4567
|
+
const existing = this.chatByRfc.get(group.rfcGroupId);
|
|
4568
|
+
if (existing) return existing;
|
|
4569
|
+
const chat = new Chat({
|
|
4570
|
+
group,
|
|
4571
|
+
kind: group.directKey ? "direct" : "group",
|
|
4572
|
+
backend: this
|
|
4573
|
+
});
|
|
4574
|
+
this.chatByRfc.set(group.rfcGroupId, chat);
|
|
4575
|
+
return chat;
|
|
4576
|
+
}
|
|
4577
|
+
// ── Resolution (build-once) ──
|
|
4578
|
+
resolve() {
|
|
4579
|
+
if (this.resolved) return Promise.resolve(this.resolved);
|
|
4580
|
+
if (this.resolvePromise) return this.resolvePromise;
|
|
4581
|
+
this.resolvePromise = this.buildResolved();
|
|
4582
|
+
return this.resolvePromise;
|
|
4583
|
+
}
|
|
4584
|
+
async buildResolved() {
|
|
4585
|
+
const userId = this.selfUserId;
|
|
4586
|
+
const deviceId = await this.deviceStore.load(userId) ?? mintDeviceId();
|
|
4587
|
+
const engine = await MlsEngine.create({
|
|
4588
|
+
userId,
|
|
4589
|
+
deviceId,
|
|
4590
|
+
groupStore: this.groupStore,
|
|
4591
|
+
kpStore: this.kpStore,
|
|
4592
|
+
sigStore: this.sigStore
|
|
4593
|
+
});
|
|
4594
|
+
const messageStore = new MessageStore(this.kv);
|
|
4595
|
+
const catalog = new GroupCatalog(this.kv);
|
|
4596
|
+
const hub = new MessageHub();
|
|
4597
|
+
const groups = new GroupMessaging(this.rt, engine, userId, deviceId, messageStore);
|
|
4598
|
+
const source = new MessageDeliverySource(
|
|
4599
|
+
this.rt,
|
|
4600
|
+
engine,
|
|
4601
|
+
hub,
|
|
4602
|
+
this.registry,
|
|
4603
|
+
messageStore,
|
|
4604
|
+
deviceId,
|
|
4605
|
+
userId
|
|
4606
|
+
);
|
|
4607
|
+
await enrollDevice(this.rt, engine, this.deviceStore, {});
|
|
4608
|
+
await this.registry.attachCatalog(catalog);
|
|
4609
|
+
const resolved = {
|
|
4610
|
+
engine,
|
|
4611
|
+
groups,
|
|
4612
|
+
source,
|
|
4613
|
+
hub,
|
|
4614
|
+
messageStore,
|
|
4615
|
+
catalog,
|
|
4616
|
+
registry: this.registry,
|
|
4617
|
+
deviceId,
|
|
4618
|
+
userId
|
|
4619
|
+
};
|
|
4620
|
+
this.resolved = resolved;
|
|
4621
|
+
await source.start();
|
|
4622
|
+
return resolved;
|
|
4623
|
+
}
|
|
4624
|
+
// ── ChatBackend impl ──
|
|
4625
|
+
async materialize(draft) {
|
|
4626
|
+
const r = await this.resolve();
|
|
4627
|
+
let group;
|
|
4628
|
+
if (draft.mode.kind === "direct") {
|
|
4629
|
+
group = await r.groups.getOrCreateDirect(draft.mode.peerUserId);
|
|
4630
|
+
} else {
|
|
4631
|
+
group = await r.groups.createGroup();
|
|
4632
|
+
for (const userId of draft.mode.members) {
|
|
4633
|
+
await r.groups.addMember(group, userId);
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
r.registry.register(group);
|
|
4637
|
+
void r.catalog.upsert({
|
|
4638
|
+
displayId: group.displayId,
|
|
4639
|
+
rfcGroupId: group.rfcGroupId,
|
|
4640
|
+
currentEpoch: group.currentEpoch,
|
|
4641
|
+
ownerUserId: group.ownerUserId,
|
|
4642
|
+
directKey: group.directKey,
|
|
4643
|
+
name: group.name
|
|
4644
|
+
});
|
|
4645
|
+
return group;
|
|
4646
|
+
}
|
|
4647
|
+
async sendText(group, text, replyTo) {
|
|
4648
|
+
const r = await this.resolve();
|
|
4649
|
+
return r.groups.sendText(group, text, replyTo);
|
|
4650
|
+
}
|
|
4651
|
+
async history(group, limit, before) {
|
|
4652
|
+
const r = await this.resolve();
|
|
4653
|
+
const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
|
|
4654
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
4655
|
+
for (const s of rows) {
|
|
4656
|
+
const cid = s.clientMsgId ?? "";
|
|
4657
|
+
if (cid && s.text !== null) {
|
|
4658
|
+
const senderUserId = s.direction === "outgoing" ? this.selfUserId : "";
|
|
4659
|
+
lookup.set(cid, { text: s.text, senderUserId });
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
return rows.map((s) => this.toChatMessage(group, s, (id) => lookup.get(id) ?? null));
|
|
4663
|
+
}
|
|
4664
|
+
toChatMessage(group, s, lookup) {
|
|
4665
|
+
const clientMsgId = s.clientMsgId ?? "";
|
|
4666
|
+
let replyTo = null;
|
|
4667
|
+
if (s.replyTo && lookup) {
|
|
4668
|
+
const ref = {
|
|
4669
|
+
v: 1,
|
|
4670
|
+
client_msg_id: s.replyTo.clientMsgId,
|
|
4671
|
+
preview: {
|
|
4672
|
+
kind: s.replyTo.previewKind,
|
|
4673
|
+
author_user_id: s.replyTo.previewAuthorUserId ?? "",
|
|
4674
|
+
body: s.replyTo.previewBody ?? void 0,
|
|
4675
|
+
body_truncated: false
|
|
4676
|
+
}
|
|
4677
|
+
};
|
|
4678
|
+
replyTo = resolveReply(ref, lookup);
|
|
4679
|
+
}
|
|
4680
|
+
return {
|
|
4681
|
+
id: `${group.displayId}#${s.serverSeq}`,
|
|
4682
|
+
kind: s.text != null ? "text" : "system",
|
|
4683
|
+
direction: s.direction,
|
|
4684
|
+
senderUserId: s.direction === "outgoing" ? this.selfUserId : null,
|
|
4685
|
+
text: s.text,
|
|
4686
|
+
serverSeq: s.serverSeq,
|
|
4687
|
+
sentAt: new Date(s.at),
|
|
4688
|
+
clientMsgId,
|
|
4689
|
+
replyTo
|
|
4690
|
+
};
|
|
1276
4691
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
4692
|
+
async members(group) {
|
|
4693
|
+
const r = await this.resolve();
|
|
4694
|
+
try {
|
|
4695
|
+
const rows = await r.groups.listMembers(group.displayId);
|
|
4696
|
+
const byUser = /* @__PURE__ */ new Map();
|
|
4697
|
+
for (const row of rows) {
|
|
4698
|
+
if (!byUser.has(row.user_id)) {
|
|
4699
|
+
byUser.set(row.user_id, {
|
|
4700
|
+
id: row.user_id,
|
|
4701
|
+
userId: row.user_id,
|
|
4702
|
+
displayName: null,
|
|
4703
|
+
role: row.role,
|
|
4704
|
+
isSelf: row.user_id === this.selfUserId
|
|
4705
|
+
});
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
return [...byUser.values()];
|
|
4709
|
+
} catch {
|
|
4710
|
+
return [];
|
|
4711
|
+
}
|
|
1280
4712
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
4713
|
+
async addMember(group, userId) {
|
|
4714
|
+
const r = await this.resolve();
|
|
4715
|
+
await r.groups.addMember(group, userId);
|
|
1284
4716
|
}
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
return typeof value === "string" ? value : fallback;
|
|
4717
|
+
async removeMember(group, userId) {
|
|
4718
|
+
const r = await this.resolve();
|
|
4719
|
+
await r.groups.removeMember(group, userId);
|
|
1289
4720
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
4721
|
+
async leave(group) {
|
|
4722
|
+
const r = await this.resolve();
|
|
4723
|
+
await r.groups.leaveGroup(group);
|
|
4724
|
+
r.registry.remove(group.rfcGroupId);
|
|
1294
4725
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
const value = this.pool.all()[key];
|
|
1298
|
-
return typeof value === "number" ? value : fallback;
|
|
4726
|
+
registerActive(group) {
|
|
4727
|
+
this.registry.register(group);
|
|
1299
4728
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
try {
|
|
1314
|
-
res = await this.transport.getVariant(key);
|
|
1315
|
-
} catch (err) {
|
|
1316
|
-
throw new BackendError("validation", {
|
|
1317
|
-
code: "invalid_flag_name",
|
|
1318
|
-
message: err instanceof Error ? err.message : String(err)
|
|
4729
|
+
setTyping(group, isTyping) {
|
|
4730
|
+
void this.resolve().then((r) => r.source.setTyping(group, isTyping));
|
|
4731
|
+
}
|
|
4732
|
+
async markRead(group, upToServerSeq) {
|
|
4733
|
+
const r = await this.resolve();
|
|
4734
|
+
await r.source.markRead(group, upToServerSeq);
|
|
4735
|
+
}
|
|
4736
|
+
subscribeLive(group, chat) {
|
|
4737
|
+
let offMsg = null;
|
|
4738
|
+
let offConv = null;
|
|
4739
|
+
void this.resolve().then((r) => {
|
|
4740
|
+
offMsg = r.hub.onMessage(group.displayId, (m) => {
|
|
4741
|
+
void chat.ingestLive(m);
|
|
1319
4742
|
});
|
|
4743
|
+
r.source.observeConversation(group);
|
|
4744
|
+
offConv = r.hub.onConv(group.displayId, (e) => chat.applyConv(e.event, e.payload));
|
|
4745
|
+
});
|
|
4746
|
+
return () => {
|
|
4747
|
+
offMsg?.();
|
|
4748
|
+
offConv?.();
|
|
4749
|
+
};
|
|
4750
|
+
}
|
|
4751
|
+
async userIdForDevice(group, deviceId) {
|
|
4752
|
+
const r = await this.resolve();
|
|
4753
|
+
try {
|
|
4754
|
+
const rows = await r.groups.listMembers(group.displayId);
|
|
4755
|
+
return rows.find((m) => m.device_id === deviceId)?.user_id ?? null;
|
|
4756
|
+
} catch {
|
|
4757
|
+
return null;
|
|
1320
4758
|
}
|
|
1321
|
-
return res.data?.name ?? null;
|
|
1322
4759
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
4760
|
+
// ── Trust (chat-scoped) ──
|
|
4761
|
+
/** List a user's devices (used by safety-number computation). */
|
|
4762
|
+
async listUserDevices(userId) {
|
|
4763
|
+
const res = await listDevices(this.rt, userId);
|
|
4764
|
+
return res.devices.map((d) => d.device_id);
|
|
4765
|
+
}
|
|
4766
|
+
};
|
|
4767
|
+
|
|
4768
|
+
// src/messaging/facade.ts
|
|
4769
|
+
var PalbeMessaging = class {
|
|
4770
|
+
coordinator;
|
|
4771
|
+
constructor(rt) {
|
|
4772
|
+
this.coordinator = new MessagingCoordinator(rt);
|
|
1326
4773
|
}
|
|
1327
4774
|
/**
|
|
1328
|
-
*
|
|
1329
|
-
*
|
|
1330
|
-
*
|
|
4775
|
+
* Warm messaging at sign-in (optional). Enrolls this device + starts the live
|
|
4776
|
+
* delivery source so incoming DMs/Welcomes drain before the chat list opens.
|
|
4777
|
+
* Never REQUIRED — any chat op self-enrolls. Idempotent.
|
|
1331
4778
|
*/
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
return this.pool.onChange(() => {
|
|
1335
|
-
const next = this.pool.all()[key];
|
|
1336
|
-
if (sameFlagValue(last, next)) return;
|
|
1337
|
-
last = next;
|
|
1338
|
-
callback(next);
|
|
1339
|
-
});
|
|
4779
|
+
async start() {
|
|
4780
|
+
await this.coordinator.ensureEnrolled();
|
|
1340
4781
|
}
|
|
1341
4782
|
/**
|
|
1342
|
-
*
|
|
1343
|
-
*
|
|
1344
|
-
*
|
|
1345
|
-
*
|
|
1346
|
-
* plain async-generator `finally` would not guarantee).
|
|
4783
|
+
* Open the ONE direct chat with `userId`. INSTANT + LOCAL — no network, nothing
|
|
4784
|
+
* created yet. Returns a draft `Chat` to show immediately; the server group +
|
|
4785
|
+
* MLS material materialize LAZILY on the first `chat.send`. Idempotent + stable:
|
|
4786
|
+
* the same two users always resolve to the SAME Chat.
|
|
1347
4787
|
*/
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
const pending = [];
|
|
1351
|
-
let finished = false;
|
|
1352
|
-
const unsubscribe = this.pool.onChange(() => {
|
|
1353
|
-
const snapshot = this.pool.all();
|
|
1354
|
-
const resolve = pending.shift();
|
|
1355
|
-
if (resolve) resolve({ value: snapshot, done: false });
|
|
1356
|
-
else queue.push(snapshot);
|
|
1357
|
-
});
|
|
1358
|
-
const finish = () => {
|
|
1359
|
-
if (finished) return;
|
|
1360
|
-
finished = true;
|
|
1361
|
-
unsubscribe();
|
|
1362
|
-
while (pending.length > 0) pending.shift()?.({ value: void 0, done: true });
|
|
1363
|
-
};
|
|
1364
|
-
return {
|
|
1365
|
-
next: () => {
|
|
1366
|
-
if (finished) return Promise.resolve({ value: void 0, done: true });
|
|
1367
|
-
const head = queue.shift();
|
|
1368
|
-
if (head !== void 0) return Promise.resolve({ value: head, done: false });
|
|
1369
|
-
return new Promise((resolve) => {
|
|
1370
|
-
pending.push(resolve);
|
|
1371
|
-
});
|
|
1372
|
-
},
|
|
1373
|
-
return: () => {
|
|
1374
|
-
finish();
|
|
1375
|
-
return Promise.resolve({ value: void 0, done: true });
|
|
1376
|
-
},
|
|
1377
|
-
throw: (error) => {
|
|
1378
|
-
finish();
|
|
1379
|
-
return Promise.reject(error);
|
|
1380
|
-
},
|
|
1381
|
-
[Symbol.asyncIterator]() {
|
|
1382
|
-
return this;
|
|
1383
|
-
}
|
|
1384
|
-
};
|
|
4788
|
+
directChat(userId) {
|
|
4789
|
+
return this.coordinator.directChat(userId);
|
|
1385
4790
|
}
|
|
1386
|
-
/**
|
|
1387
|
-
|
|
1388
|
-
|
|
4791
|
+
/**
|
|
4792
|
+
* Open a multi-party group chat. LOCAL + LAZY like `directChat`: pick the
|
|
4793
|
+
* members, get a draft Chat, the group is created on the FIRST `chat.send`.
|
|
4794
|
+
*/
|
|
4795
|
+
groupChat(opts = {}) {
|
|
4796
|
+
return this.coordinator.groupChat(opts.members ?? []);
|
|
4797
|
+
}
|
|
4798
|
+
/** Look up a chat by id (e.g. from a push payload / deep link). null if unknown. */
|
|
4799
|
+
chat(id) {
|
|
4800
|
+
return this.coordinator.chatById(id);
|
|
4801
|
+
}
|
|
4802
|
+
/** The observable chat list (DMs + groups, active only). Read it after an
|
|
4803
|
+
* `onChatsChange` subscription to render the inbox. */
|
|
4804
|
+
get chats() {
|
|
4805
|
+
return this.coordinator.chats;
|
|
4806
|
+
}
|
|
4807
|
+
/** Subscribe to chat-list changes (a chat added/removed/hydrated). */
|
|
4808
|
+
onChatsChange(cb) {
|
|
4809
|
+
return this.coordinator.onChatsChange(cb);
|
|
1389
4810
|
}
|
|
1390
4811
|
};
|
|
1391
4812
|
|
|
@@ -1856,10 +5277,10 @@ var RealtimeSocket = class {
|
|
|
1856
5277
|
this.flushPending(topic);
|
|
1857
5278
|
}
|
|
1858
5279
|
flushPending(topic) {
|
|
1859
|
-
const
|
|
1860
|
-
if (
|
|
5280
|
+
const ready2 = this.pendingBroadcasts.filter((p) => p.topic === topic);
|
|
5281
|
+
if (ready2.length === 0) return;
|
|
1861
5282
|
this.pendingBroadcasts = this.pendingBroadcasts.filter((p) => p.topic !== topic);
|
|
1862
|
-
for (const p of
|
|
5283
|
+
for (const p of ready2) this.sendBroadcast(p.topic, p.event, p.payload);
|
|
1863
5284
|
}
|
|
1864
5285
|
startHeartbeat() {
|
|
1865
5286
|
this.stopHeartbeat();
|
|
@@ -2195,7 +5616,7 @@ function localStorageSessionStorage(key = DEFAULT_KEY) {
|
|
|
2195
5616
|
}
|
|
2196
5617
|
|
|
2197
5618
|
// src/version.ts
|
|
2198
|
-
var VERSION = "1.0
|
|
5619
|
+
var VERSION = "1.1.0";
|
|
2199
5620
|
|
|
2200
5621
|
// src/internal.ts
|
|
2201
5622
|
function getRuntime() {
|
|
@@ -2358,6 +5779,12 @@ function createClientProxy(resolveRt, nsAccessor) {
|
|
|
2358
5779
|
},
|
|
2359
5780
|
get analytics() {
|
|
2360
5781
|
return resolveRt().analytics;
|
|
5782
|
+
},
|
|
5783
|
+
get calls() {
|
|
5784
|
+
return resolveRt().calls;
|
|
5785
|
+
},
|
|
5786
|
+
get messaging() {
|
|
5787
|
+
return resolveRt().messaging;
|
|
2361
5788
|
}
|
|
2362
5789
|
};
|
|
2363
5790
|
return new Proxy(base, {
|
|
@@ -2382,10 +5809,13 @@ var pb = createClientProxy(getRuntime, getNamespace);
|
|
|
2382
5809
|
BackendError,
|
|
2383
5810
|
PalbeAnalytics,
|
|
2384
5811
|
PalbeAuth,
|
|
5812
|
+
PalbeCalls,
|
|
2385
5813
|
PalbeFlags,
|
|
5814
|
+
PalbeMessaging,
|
|
2386
5815
|
PalbeRealtime,
|
|
2387
5816
|
RealtimeChannel,
|
|
2388
5817
|
VERSION,
|
|
5818
|
+
initMls,
|
|
2389
5819
|
isBackendError,
|
|
2390
5820
|
localStorageSessionStorage,
|
|
2391
5821
|
memorySessionStorage,
|