@palbase/web 1.0.0 → 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/internal.cjs CHANGED
@@ -464,7 +464,16 @@ function palbeState() {
464
464
  }
465
465
 
466
466
  // src/namespaces.ts
467
- var FIXED_SURFACE = /* @__PURE__ */ new Set(["call", "upload", "auth", "flags", "realtime", "analytics"]);
467
+ var FIXED_SURFACE = /* @__PURE__ */ new Set([
468
+ "call",
469
+ "upload",
470
+ "auth",
471
+ "flags",
472
+ "realtime",
473
+ "analytics",
474
+ "calls",
475
+ "messaging"
476
+ ]);
468
477
  function reservedNamespaceError(key) {
469
478
  return new BackendError("validation", {
470
479
  code: "reserved_namespace",
@@ -1100,9 +1109,10 @@ var AuthClient = class {
1100
1109
  };
1101
1110
 
1102
1111
  // src/api-key.ts
1112
+ var API_KEY_RE = /^pb_([^_]+)_[cs][A-Za-z0-9]{20}$/;
1103
1113
  function endpointRefFromApiKey(apiKey) {
1104
- const parts = apiKey.split("_");
1105
- return parts.length >= 3 && parts[0] === "pb" ? parts[1] ?? "" : "";
1114
+ const m = API_KEY_RE.exec(apiKey);
1115
+ return m ? m[1] ?? "" : "";
1106
1116
  }
1107
1117
 
1108
1118
  // src/analytics-facade.ts
@@ -1695,6 +1705,220 @@ var PalbeAuth = class {
1695
1705
  }
1696
1706
  };
1697
1707
 
1708
+ // src/calls/facade.ts
1709
+ var import_livekit_client = require("livekit-client");
1710
+ var Call = class {
1711
+ id;
1712
+ _state = "connecting";
1713
+ _participants = /* @__PURE__ */ new Map();
1714
+ _listeners = /* @__PURE__ */ new Set();
1715
+ _room;
1716
+ _keyProvider;
1717
+ /** @internal — obtain via PalbeCalls.start() / .accept() */
1718
+ constructor(callId, room, keyProvider) {
1719
+ this.id = callId;
1720
+ this._room = room;
1721
+ this._keyProvider = keyProvider;
1722
+ this._wireRoomEvents();
1723
+ }
1724
+ // ─── State ──────────────────────────────────────────────────────────────
1725
+ get state() {
1726
+ return this._state;
1727
+ }
1728
+ /** Snapshot of current remote participants (does NOT include the local participant). */
1729
+ get participants() {
1730
+ return Array.from(this._participants.values());
1731
+ }
1732
+ get localParticipant() {
1733
+ return this._room.localParticipant;
1734
+ }
1735
+ // ─── Subscribe ──────────────────────────────────────────────────────────
1736
+ /**
1737
+ * Subscribe to call changes (state + participant updates).
1738
+ * Returns an `Unsubscribe` function — matches the SDK's flags/auth pattern.
1739
+ */
1740
+ subscribe(callback) {
1741
+ this._listeners.add(callback);
1742
+ return () => {
1743
+ this._listeners.delete(callback);
1744
+ };
1745
+ }
1746
+ // ─── Controls ───────────────────────────────────────────────────────────
1747
+ /** Mute / unmute the local microphone. */
1748
+ async mute(on) {
1749
+ await this._room.localParticipant.setMicrophoneEnabled(!on);
1750
+ }
1751
+ /** Enable / disable the local camera. */
1752
+ async setCamera(on) {
1753
+ await this._room.localParticipant.setCameraEnabled(on);
1754
+ }
1755
+ /**
1756
+ * Set a raw frame encryption key (ArrayBuffer) for this call.
1757
+ * Design seam for SP-1.x web-MLS: call this with the MLS exporter secret
1758
+ * to enable frame-level E2EE via ExternalE2EEKeyProvider.
1759
+ *
1760
+ * NOTE: The room must have been opened with `e2ee` options for this to take
1761
+ * effect. Currently calls connect WITHOUT e2ee — see module docstring.
1762
+ *
1763
+ * TODO(SP-1-web): wire this to MLS-derived key on session join.
1764
+ */
1765
+ async setMediaKey(key) {
1766
+ await this._keyProvider.setKey(key);
1767
+ }
1768
+ /** Leave the call and disconnect from the LiveKit room. */
1769
+ async leave() {
1770
+ await this._room.disconnect();
1771
+ this._setState("ended");
1772
+ }
1773
+ // ─── Internal ───────────────────────────────────────────────────────────
1774
+ _setState(next) {
1775
+ if (this._state === next) return;
1776
+ this._state = next;
1777
+ this._emit();
1778
+ }
1779
+ _emit() {
1780
+ for (const cb of this._listeners) cb(this);
1781
+ }
1782
+ _upsertParticipant(p) {
1783
+ const existing = this._participants.get(p.identity);
1784
+ const entry = {
1785
+ identity: p.identity,
1786
+ isSpeaking: p.isSpeaking,
1787
+ audioMuted: !p.isMicrophoneEnabled,
1788
+ videoEnabled: p.isCameraEnabled,
1789
+ videoElement: existing?.videoElement ?? null,
1790
+ audioElement: existing?.audioElement ?? null
1791
+ };
1792
+ this._participants.set(p.identity, entry);
1793
+ }
1794
+ _attachTrack(track, participant) {
1795
+ const element = track.attach();
1796
+ const entry = this._participants.get(participant.identity);
1797
+ if (track.kind === import_livekit_client.Track.Kind.Video) {
1798
+ if (entry) {
1799
+ entry.videoElement = element;
1800
+ }
1801
+ } else if (track.kind === import_livekit_client.Track.Kind.Audio) {
1802
+ if (entry) {
1803
+ entry.audioElement = element;
1804
+ element.play().catch(() => {
1805
+ });
1806
+ }
1807
+ }
1808
+ this._emit();
1809
+ }
1810
+ _wireRoomEvents() {
1811
+ this._room.on(import_livekit_client.RoomEvent.Connected, () => {
1812
+ this._setState("active");
1813
+ }).on(
1814
+ import_livekit_client.RoomEvent.TrackSubscribed,
1815
+ (track, _pub, participant) => {
1816
+ this._upsertParticipant(participant);
1817
+ this._attachTrack(track, participant);
1818
+ }
1819
+ ).on(
1820
+ import_livekit_client.RoomEvent.TrackUnsubscribed,
1821
+ (_track, _pub, participant) => {
1822
+ this._upsertParticipant(participant);
1823
+ this._emit();
1824
+ }
1825
+ ).on(import_livekit_client.RoomEvent.ParticipantConnected, (participant) => {
1826
+ this._upsertParticipant(participant);
1827
+ this._emit();
1828
+ }).on(import_livekit_client.RoomEvent.ParticipantDisconnected, (participant) => {
1829
+ this._participants.delete(participant.identity);
1830
+ this._emit();
1831
+ }).on(import_livekit_client.RoomEvent.ActiveSpeakersChanged, (speakers) => {
1832
+ for (const [id, entry] of this._participants) {
1833
+ entry.isSpeaking = speakers.some((s) => s.identity === id);
1834
+ }
1835
+ this._emit();
1836
+ }).on(import_livekit_client.RoomEvent.Disconnected, () => {
1837
+ this._setState("ended");
1838
+ });
1839
+ }
1840
+ };
1841
+ var PalbeCalls = class {
1842
+ rt;
1843
+ constructor(rt) {
1844
+ this.rt = rt;
1845
+ }
1846
+ /**
1847
+ * Start a new call in a messaging group.
1848
+ *
1849
+ * @param groupId - Messaging group display-id (`grp_<uuidv7>`)
1850
+ * @param media - Which media tracks to publish (default: audio + video)
1851
+ * @returns A `Call` in `connecting` state; transitions to `active` once LiveKit connects.
1852
+ *
1853
+ * @throws BackendError('network', { code: 'call_service_unavailable' }) if LiveKit SFU is down (503)
1854
+ * @throws BackendError('forbidden', { code: 'not_group_member' }) if the caller is not a member (403)
1855
+ * @throws BackendError('conflict', { code: 'call_room_full' }) if the room is full (409)
1856
+ */
1857
+ async start(groupId, { media = ["audio", "video"] } = {}) {
1858
+ if (!groupId) {
1859
+ throw new BackendError("validation", {
1860
+ code: "invalid_group_id",
1861
+ message: "groupId must be a non-empty string"
1862
+ });
1863
+ }
1864
+ const res = await palbeRequest(
1865
+ this.rt,
1866
+ "POST",
1867
+ `/v1/messaging/groups/${encodeURIComponent(groupId)}/calls`,
1868
+ { body: { media } }
1869
+ );
1870
+ return this._connect(res.call_id, res.token, res.edge_url, media);
1871
+ }
1872
+ /**
1873
+ * Accept an incoming call in a messaging group.
1874
+ *
1875
+ * @param groupId - Messaging group display-id
1876
+ * @param callId - Call identifier (from the push notification payload)
1877
+ * @param media - Which media tracks to publish (default: audio + video)
1878
+ */
1879
+ async accept(groupId, callId, { media = ["audio", "video"] } = {}) {
1880
+ const res = await palbeRequest(
1881
+ this.rt,
1882
+ "POST",
1883
+ `/v1/messaging/groups/${encodeURIComponent(groupId)}/calls/${encodeURIComponent(callId)}/accept`,
1884
+ { body: {} }
1885
+ );
1886
+ return this._connect(callId, res.token, res.edge_url, media);
1887
+ }
1888
+ /**
1889
+ * Decline an incoming call.
1890
+ *
1891
+ * @param groupId - Messaging group display-id
1892
+ * @param callId - Call identifier
1893
+ * @param reason - Optional decline reason string
1894
+ */
1895
+ async decline(groupId, callId, reason) {
1896
+ await palbeRequest(
1897
+ this.rt,
1898
+ "POST",
1899
+ `/v1/messaging/groups/${encodeURIComponent(groupId)}/calls/${encodeURIComponent(callId)}/decline`,
1900
+ { body: { reason: reason ?? "" } }
1901
+ );
1902
+ }
1903
+ // ─── Internal ─────────────────────────────────────────────────────────
1904
+ async _connect(callId, token, edgeUrl, media) {
1905
+ const keyProvider = new import_livekit_client.ExternalE2EEKeyProvider();
1906
+ const room = new import_livekit_client.Room({
1907
+ adaptiveStream: true,
1908
+ dynacast: true
1909
+ });
1910
+ const call = new Call(callId, room, keyProvider);
1911
+ await room.connect(edgeUrl, token);
1912
+ if (media.includes("audio")) {
1913
+ await room.localParticipant.setMicrophoneEnabled(true);
1914
+ }
1915
+ if (media.includes("video")) {
1916
+ await room.localParticipant.setCameraEnabled(true);
1917
+ }
1918
+ return call;
1919
+ }
1920
+ };
1921
+
1698
1922
  // ../modules/flags/dist/index.js
1699
1923
  var FLAG_NAME_RE = /^[a-zA-Z0-9_-]+$/;
1700
1924
  var FlagsClient = class {
@@ -2197,194 +2421,3397 @@ function resolveStorage() {
2197
2421
  return null;
2198
2422
  }
2199
2423
  }
2200
- function resolveVisibility() {
2201
- if (typeof document === "undefined") {
2202
- return null;
2424
+ function resolveVisibility() {
2425
+ if (typeof document === "undefined") {
2426
+ return null;
2427
+ }
2428
+ const doc = document;
2429
+ return {
2430
+ isHidden: () => doc.visibilityState === "hidden",
2431
+ onVisibilityChange: (cb) => {
2432
+ doc.addEventListener("visibilitychange", cb);
2433
+ return () => doc.removeEventListener("visibilitychange", cb);
2434
+ }
2435
+ };
2436
+ }
2437
+
2438
+ // src/flags-facade.ts
2439
+ function palbeAuthAdapter(auth) {
2440
+ return {
2441
+ onAuthStateChange(callback) {
2442
+ const unsubscribe = auth.onAuthStateChange(() => {
2443
+ const signedIn = auth.isSignedIn;
2444
+ callback(signedIn ? "SIGNED_IN" : "SIGNED_OUT", signedIn ? { signedIn } : null);
2445
+ });
2446
+ return { data: { subscription: { unsubscribe } } };
2447
+ }
2448
+ };
2449
+ }
2450
+ function serverPoolEnv() {
2451
+ return {
2452
+ now: () => Date.now(),
2453
+ setInterval: () => {
2454
+ throw new Error("palbe flags: polling is disabled server-side");
2455
+ },
2456
+ clearInterval: () => {
2457
+ },
2458
+ storage: null,
2459
+ visibility: null
2460
+ };
2461
+ }
2462
+ function sameFlagValue(a, b) {
2463
+ if (Object.is(a, b)) return true;
2464
+ if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) {
2465
+ return JSON.stringify(a) === JSON.stringify(b);
2466
+ }
2467
+ return false;
2468
+ }
2469
+ var PalbeFlags = class {
2470
+ transport;
2471
+ pool;
2472
+ constructor(rt) {
2473
+ this.transport = new FlagsClient(rt.http);
2474
+ const browser = typeof document !== "undefined";
2475
+ const ref = endpointRefFromApiKey(rt.config.apiKey);
2476
+ const options = {
2477
+ auth: palbeAuthAdapter(rt.auth),
2478
+ ...ref ? { storageKey: `palbe.flags.${ref}` } : {},
2479
+ ...browser ? {} : { pollIntervalMs: 0, env: serverPoolEnv() }
2480
+ };
2481
+ this.pool = new FlagsPool(this.transport, options);
2482
+ if (browser) this.pool.start();
2483
+ }
2484
+ /** Resolves once the first snapshot (or persisted hydrate) is available. Auto-starts the pool. */
2485
+ ready() {
2486
+ return this.pool.ready();
2487
+ }
2488
+ /** Force an immediate re-snapshot. */
2489
+ refresh() {
2490
+ return this.pool.refresh();
2491
+ }
2492
+ /** Frozen snapshot of all cached values (identity-stable until a change). */
2493
+ all() {
2494
+ return this.pool.all();
2495
+ }
2496
+ /** Raw cached value for `key`, or `undefined` when not in the cache. */
2497
+ get(key) {
2498
+ return this.pool.all()[key];
2499
+ }
2500
+ /** `true` only when the cached value is strictly `true`; `fallback` when the key is absent. */
2501
+ isEnabled(key, fallback = false) {
2502
+ return this.pool.isEnabled(key, fallback);
2503
+ }
2504
+ /** Alias of {@link isEnabled} (iOS parity). */
2505
+ bool(key, fallback = false) {
2506
+ return this.isEnabled(key, fallback);
2507
+ }
2508
+ /** Cached value when it is a string, else `fallback`. */
2509
+ getString(key, fallback) {
2510
+ const value = this.pool.all()[key];
2511
+ return typeof value === "string" ? value : fallback;
2512
+ }
2513
+ /** Cached value when it is an integer number, else `fallback`. */
2514
+ getInt(key, fallback) {
2515
+ const value = this.pool.all()[key];
2516
+ return typeof value === "number" && Number.isInteger(value) ? value : fallback;
2517
+ }
2518
+ /** Cached value when it is a number (integers included), else `fallback`. */
2519
+ getDouble(key, fallback) {
2520
+ const value = this.pool.all()[key];
2521
+ return typeof value === "number" ? value : fallback;
2522
+ }
2523
+ /**
2524
+ * Resolve the multivariate variant for `key` — the variant name, or `null`
2525
+ * when the flag has no variant (or the read fails).
2526
+ *
2527
+ * Wire failures (network errors, 404, etc.) resolve to `null`.
2528
+ * Invalid flag names throw `BackendError('validation', { code: 'invalid_flag_name' })`.
2529
+ *
2530
+ * DEVIATION from iOS sync-cache parity: the platform does not propagate
2531
+ * variant metadata into the user-flags snapshot/delta cache, so this is an
2532
+ * ASYNC transport read (`GET /v1/flags/{key}/variant`), not a cache lookup.
2533
+ */
2534
+ async getVariant(key) {
2535
+ let res;
2536
+ try {
2537
+ res = await this.transport.getVariant(key);
2538
+ } catch (err) {
2539
+ throw new BackendError("validation", {
2540
+ code: "invalid_flag_name",
2541
+ message: err instanceof Error ? err.message : String(err)
2542
+ });
2543
+ }
2544
+ return res.data?.name ?? null;
2545
+ }
2546
+ /** Subscribe to any change in the cached flag set. */
2547
+ onChange(callback) {
2548
+ return this.pool.onChange(callback);
2549
+ }
2550
+ /**
2551
+ * Observe ONE key: fires only when that key's value actually changes
2552
+ * (per {@link sameFlagValue} — structural compare for objects), with the
2553
+ * new value (`undefined` = deleted). P5 React-hook substrate.
2554
+ */
2555
+ subscribeKey(key, callback) {
2556
+ let last = this.pool.all()[key];
2557
+ return this.pool.onChange(() => {
2558
+ const next = this.pool.all()[key];
2559
+ if (sameFlagValue(last, next)) return;
2560
+ last = next;
2561
+ callback(next);
2562
+ });
2563
+ }
2564
+ /**
2565
+ * Async iteration over flag changes: yields the new {@link all} view on
2566
+ * every pool change notification. The listener is detached when the
2567
+ * consumer `break`s/`return`s/`throw`s — including while a `next()` is
2568
+ * still pending (it resolves `{ done: true }` instead of hanging, which a
2569
+ * plain async-generator `finally` would not guarantee).
2570
+ */
2571
+ changes() {
2572
+ const queue = [];
2573
+ const pending = [];
2574
+ let finished = false;
2575
+ const unsubscribe = this.pool.onChange(() => {
2576
+ const snapshot = this.pool.all();
2577
+ const resolve = pending.shift();
2578
+ if (resolve) resolve({ value: snapshot, done: false });
2579
+ else queue.push(snapshot);
2580
+ });
2581
+ const finish = () => {
2582
+ if (finished) return;
2583
+ finished = true;
2584
+ unsubscribe();
2585
+ while (pending.length > 0) pending.shift()?.({ value: void 0, done: true });
2586
+ };
2587
+ return {
2588
+ next: () => {
2589
+ if (finished) return Promise.resolve({ value: void 0, done: true });
2590
+ const head = queue.shift();
2591
+ if (head !== void 0) return Promise.resolve({ value: head, done: false });
2592
+ return new Promise((resolve) => {
2593
+ pending.push(resolve);
2594
+ });
2595
+ },
2596
+ return: () => {
2597
+ finish();
2598
+ return Promise.resolve({ value: void 0, done: true });
2599
+ },
2600
+ throw: (error) => {
2601
+ finish();
2602
+ return Promise.reject(error);
2603
+ },
2604
+ [Symbol.asyncIterator]() {
2605
+ return this;
2606
+ }
2607
+ };
2608
+ }
2609
+ /** Stop polling and detach all listeners (auth + visibility + subscribers). */
2610
+ destroy() {
2611
+ this.pool.destroy();
2612
+ }
2613
+ };
2614
+
2615
+ // src/messaging/util.ts
2616
+ function toBase64(bytes) {
2617
+ if (typeof Buffer !== "undefined") {
2618
+ return Buffer.from(bytes).toString("base64");
2619
+ }
2620
+ let binary = "";
2621
+ const chunk = 32768;
2622
+ for (let i = 0; i < bytes.length; i += chunk) {
2623
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
2624
+ }
2625
+ return btoa(binary);
2626
+ }
2627
+ function fromBase64(b64) {
2628
+ if (typeof Buffer !== "undefined") {
2629
+ return new Uint8Array(Buffer.from(b64, "base64"));
2630
+ }
2631
+ const binary = atob(b64);
2632
+ const out = new Uint8Array(binary.length);
2633
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
2634
+ return out;
2635
+ }
2636
+ function bytesToHex(bytes) {
2637
+ let s = "";
2638
+ for (let i = 0; i < bytes.length; i++) {
2639
+ s += (bytes[i] ?? 0).toString(16).padStart(2, "0");
2640
+ }
2641
+ return s;
2642
+ }
2643
+ var utf8Encoder = new TextEncoder();
2644
+ var utf8Decoder = new TextDecoder();
2645
+ var encodeUtf8 = (s) => utf8Encoder.encode(s);
2646
+ var decodeUtf8 = (b) => utf8Decoder.decode(b);
2647
+ var deviceIdBytes = (deviceId) => encodeUtf8(deviceId);
2648
+ function randomId() {
2649
+ return crypto.randomUUID();
2650
+ }
2651
+ function mintDeviceId() {
2652
+ return `mdv_${uuidV7()}`;
2653
+ }
2654
+ function uuidV7() {
2655
+ const bytes = new Uint8Array(16);
2656
+ crypto.getRandomValues(bytes);
2657
+ const ts = Date.now();
2658
+ const view = new DataView(bytes.buffer);
2659
+ view.setUint32(0, Math.floor(ts / 2 ** 16));
2660
+ view.setUint16(4, ts & 65535);
2661
+ view.setUint8(6, view.getUint8(6) & 15 | 112);
2662
+ view.setUint8(8, view.getUint8(8) & 63 | 128);
2663
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
2664
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
2665
+ }
2666
+ function directKey(userA, userB) {
2667
+ const [a, b] = [userA, userB].sort();
2668
+ return `dm:${a}|${b}`;
2669
+ }
2670
+ function mintClientMsgId() {
2671
+ const b = new Uint8Array(16);
2672
+ crypto.getRandomValues(b);
2673
+ return `msg_${Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("")}`;
2674
+ }
2675
+ function truncateQuote(s) {
2676
+ const scalars = [...s];
2677
+ if (scalars.length <= 240) return [s, false];
2678
+ return [scalars.slice(0, 240).join(""), true];
2679
+ }
2680
+
2681
+ // src/messaging/wire.ts
2682
+ var GROUPS = "/v1/messaging/groups";
2683
+ var DEVICES = "/v1/messaging/devices";
2684
+ var KEYDIR = "/v1/messaging/keydir";
2685
+ var seg = (s) => encodeURIComponent(s);
2686
+ var MessagingPaths = {
2687
+ groups: GROUPS,
2688
+ groupsDirect: `${GROUPS}/direct`,
2689
+ group: (displayId) => `${GROUPS}/${seg(displayId)}`,
2690
+ groupMetadata: (displayId) => `${GROUPS}/${seg(displayId)}/metadata`,
2691
+ groupMembers: (displayId) => `${GROUPS}/${seg(displayId)}/members`,
2692
+ groupMemberDevice: (displayId, deviceId) => `${GROUPS}/${seg(displayId)}/members/${seg(deviceId)}`,
2693
+ groupMessages: (displayId) => `${GROUPS}/${seg(displayId)}/messages`,
2694
+ groupCommits: (displayId) => `${GROUPS}/${seg(displayId)}/commits`,
2695
+ groupRead: (displayId) => `${GROUPS}/${seg(displayId)}/read`,
2696
+ deviceWelcomes: (deviceId) => `${DEVICES}/${seg(deviceId)}/welcomes`,
2697
+ deviceQueue: (deviceId) => `${DEVICES}/${seg(deviceId)}/queue`,
2698
+ deviceQueueAck: (deviceId) => `${DEVICES}/${seg(deviceId)}/queue/ack`,
2699
+ keydirDevices: `${KEYDIR}/devices`,
2700
+ keydirDeviceKeyPackages: (deviceId) => `${KEYDIR}/devices/${seg(deviceId)}/keypackages`,
2701
+ keydirUserDevices: (userId) => `${KEYDIR}/users/${seg(userId)}/devices`,
2702
+ keydirUserClaim: (userId) => `${KEYDIR}/users/${seg(userId)}/keypackages/claim`,
2703
+ keydirPinsVerify: `${KEYDIR}/pins/verify`,
2704
+ prefs: "/v1/messaging/prefs"
2705
+ };
2706
+ var ContentType = {
2707
+ application: 1,
2708
+ proposal: 2,
2709
+ commit: 3
2710
+ };
2711
+
2712
+ // src/messaging/keydir.ts
2713
+ async function enrollDevice(rt, engine, deviceStore, opts = {}) {
2714
+ const keyPackageCount = opts.keyPackageCount ?? 10;
2715
+ const deviceId = engine.deviceId;
2716
+ const alreadyEnrolled = await deviceStore.load(engine.userId) === deviceId;
2717
+ if (!alreadyEnrolled) {
2718
+ const material = await engine.enrollmentMaterial();
2719
+ const body = {
2720
+ device_id: deviceId,
2721
+ platform: "web",
2722
+ device_name: opts.deviceName,
2723
+ cipher_suite: 1,
2724
+ signature_key: toBase64(material.signatureKey),
2725
+ credential: toBase64(material.credential),
2726
+ capabilities: toBase64(material.capabilities)
2727
+ };
2728
+ await palbeRequest(rt, "POST", MessagingPaths.keydirDevices, { body });
2729
+ await deviceStore.save(engine.userId, deviceId);
2730
+ }
2731
+ if (keyPackageCount > 0) {
2732
+ const raws = [];
2733
+ for (let i = 0; i < keyPackageCount; i++) {
2734
+ raws.push(toBase64(await engine.generateKeyPackage()));
2735
+ }
2736
+ await replenish(rt, deviceId, raws);
2737
+ }
2738
+ return deviceId;
2739
+ }
2740
+ async function replenish(rt, deviceId, rawKeyPackagesB64) {
2741
+ const body = { key_packages: rawKeyPackagesB64 };
2742
+ await palbeRequest(rt, "POST", MessagingPaths.keydirDeviceKeyPackages(deviceId), { body });
2743
+ }
2744
+ async function claimKeyPackages(rt, userId) {
2745
+ return palbeRequest(rt, "POST", MessagingPaths.keydirUserClaim(userId), {
2746
+ body: {}
2747
+ });
2748
+ }
2749
+ async function listDevices(rt, userId) {
2750
+ return palbeRequest(rt, "GET", MessagingPaths.keydirUserDevices(userId));
2751
+ }
2752
+
2753
+ // src/messaging/group-messaging.ts
2754
+ function encodeEnvelope(args) {
2755
+ const env = {
2756
+ v: 1,
2757
+ type: "text",
2758
+ client_msg_id: args.clientMsgId,
2759
+ text: args.text,
2760
+ ...args.replyTo ? { reply_to: args.replyTo } : {}
2761
+ };
2762
+ return encodeUtf8(JSON.stringify(env));
2763
+ }
2764
+ function decodeEnvelope(bytes) {
2765
+ const s = decodeUtf8(bytes);
2766
+ try {
2767
+ const o = JSON.parse(s);
2768
+ if (typeof o === "object" && o !== null && o.type === "text" && typeof o.v === "number") {
2769
+ return {
2770
+ text: o.text ?? null,
2771
+ clientMsgId: o.client_msg_id ?? "",
2772
+ replyTo: o.reply_to ?? null
2773
+ };
2774
+ }
2775
+ if (typeof o === "object" && o !== null && o.type === "text") {
2776
+ return { text: o.text ?? null, clientMsgId: "", replyTo: null };
2777
+ }
2778
+ if (typeof o === "object" && o !== null && o.type === "media")
2779
+ return { text: null, clientMsgId: "", replyTo: null };
2780
+ } catch {
2781
+ }
2782
+ return { text: s, clientMsgId: "", replyTo: null };
2783
+ }
2784
+ function resolveReply(ref, lookup) {
2785
+ const parent = lookup(ref.client_msg_id);
2786
+ if (parent !== null) {
2787
+ return {
2788
+ parentClientMsgId: ref.client_msg_id,
2789
+ state: "resolved",
2790
+ quoteSenderUserId: parent.senderUserId,
2791
+ quoteText: parent.text,
2792
+ quoteKind: ref.preview?.kind ?? "text"
2793
+ };
2794
+ }
2795
+ return {
2796
+ parentClientMsgId: ref.client_msg_id,
2797
+ state: "unavailable",
2798
+ quoteSenderUserId: ref.preview?.author_user_id ?? "",
2799
+ quoteText: ref.preview?.body ?? null,
2800
+ quoteKind: ref.preview?.kind ?? "text"
2801
+ };
2802
+ }
2803
+ var DEFAULT_REBASE = { maxAttempts: 6, baseDelayMs: 80, jitterMs: 120 };
2804
+ var RebaseExhausted = class extends Error {
2805
+ constructor(attempts, lastKnownEpoch) {
2806
+ super(`commit rebase exhausted after ${attempts} attempts`);
2807
+ this.attempts = attempts;
2808
+ this.lastKnownEpoch = lastKnownEpoch;
2809
+ this.name = "RebaseExhausted";
2810
+ }
2811
+ attempts;
2812
+ lastKnownEpoch;
2813
+ };
2814
+ var GroupMessaging = class {
2815
+ constructor(rt, engine, selfUserId, selfDeviceId, messageStore, rebase = DEFAULT_REBASE) {
2816
+ this.rt = rt;
2817
+ this.engine = engine;
2818
+ this.selfUserId = selfUserId;
2819
+ this.selfDeviceId = selfDeviceId;
2820
+ this.messageStore = messageStore;
2821
+ this.rebase = rebase;
2822
+ }
2823
+ rt;
2824
+ engine;
2825
+ selfUserId;
2826
+ selfDeviceId;
2827
+ messageStore;
2828
+ rebase;
2829
+ // ── Create ──
2830
+ /** Create a group: mint the rfc_group_id, POST create, return the server group.
2831
+ * No name sealing on web (metadata omitted — server-blind, label fallback). */
2832
+ async createGroup(cipherSuite = 1) {
2833
+ const rfcGroupId = await this.engine.createGroup();
2834
+ const body = {
2835
+ rfc_group_id: toBase64(rfcGroupId),
2836
+ cipher_suite: cipherSuite,
2837
+ creator_device_id: this.selfDeviceId,
2838
+ creator_leaf_index: 0
2839
+ };
2840
+ const res = await palbeRequest(this.rt, "POST", MessagingPaths.groups, {
2841
+ body
2842
+ });
2843
+ return {
2844
+ displayId: res.display_id,
2845
+ rfcGroupId: res.rfc_group_id,
2846
+ currentEpoch: res.current_epoch,
2847
+ ownerUserId: this.selfUserId,
2848
+ directKey: null,
2849
+ name: null
2850
+ };
2851
+ }
2852
+ // ── Direct (1:1) get-or-create ──
2853
+ /** Materialize a DIRECT chat keyed by the stable direct_key (sorted user pair).
2854
+ * EXISTS → adopt (the Welcome drains via the pump); ABSENT → create + add peer. */
2855
+ async getOrCreateDirect(peerUserId) {
2856
+ const dk = directKey(this.selfUserId, peerUserId);
2857
+ const rfcGroupId = await this.engine.createGroup();
2858
+ const body = {
2859
+ direct_key: dk,
2860
+ peer_user_id: peerUserId,
2861
+ rfc_group_id: toBase64(rfcGroupId),
2862
+ cipher_suite: 1,
2863
+ creator_device_id: this.selfDeviceId,
2864
+ creator_leaf_index: 0
2865
+ };
2866
+ const res = await palbeRequest(
2867
+ this.rt,
2868
+ "POST",
2869
+ MessagingPaths.groupsDirect,
2870
+ { body }
2871
+ );
2872
+ if (res.exists === true) {
2873
+ await this.engine.clearPendingCommit(rfcGroupId);
2874
+ return {
2875
+ displayId: res.display_id,
2876
+ rfcGroupId: res.rfc_group_id,
2877
+ currentEpoch: res.current_epoch,
2878
+ ownerUserId: res.owner_user_id ?? null,
2879
+ directKey: dk,
2880
+ name: null
2881
+ };
2882
+ }
2883
+ const group = {
2884
+ displayId: res.display_id,
2885
+ rfcGroupId: res.rfc_group_id,
2886
+ currentEpoch: res.current_epoch,
2887
+ ownerUserId: this.selfUserId,
2888
+ directKey: dk,
2889
+ name: null
2890
+ };
2891
+ const claimed = await claimKeyPackages(this.rt, peerUserId);
2892
+ await this.addMember(group, peerUserId, claimed);
2893
+ return group;
2894
+ }
2895
+ // ── Members ──
2896
+ /** The group roster (device leaves). The coordinator dedups to users. */
2897
+ async listMembers(displayId) {
2898
+ const res = await palbeRequest(
2899
+ this.rt,
2900
+ "GET",
2901
+ MessagingPaths.groupMembers(displayId)
2902
+ );
2903
+ return res.members;
2904
+ }
2905
+ /** Add a member: claim → stage the add commit → run the rebase loop. SP-1.6:
2906
+ * EVERY device of the user joins in ONE atomic commit (one shared Welcome). */
2907
+ async addMember(group, userId, claimed) {
2908
+ const claim = claimed ?? await claimKeyPackages(this.rt, userId);
2909
+ const adds = claim.key_packages.map((kp) => {
2910
+ try {
2911
+ return { deviceId: kp.device_id, kp: fromBase64(kp.key_package) };
2912
+ } catch {
2913
+ return null;
2914
+ }
2915
+ }).filter((x) => x !== null);
2916
+ if (adds.length === 0) {
2917
+ throw new BackendError("validation", {
2918
+ code: "no_keypackage",
2919
+ message: "no claimable key package for user",
2920
+ status: 422
2921
+ });
2922
+ }
2923
+ const userIds = adds.map(() => userId);
2924
+ await this.commitWithRebase(group.rfcGroupId, async () => {
2925
+ const out = await this.engine.commitChanges(
2926
+ fromBase64(group.rfcGroupId),
2927
+ adds.map((a) => a.kp),
2928
+ []
2929
+ );
2930
+ const welcomeB64 = out.welcome ? toBase64(out.welcome) : "";
2931
+ const welcomes = adds.map((a) => ({
2932
+ recipient_device_id: a.deviceId,
2933
+ recipient_user_id: userId,
2934
+ welcome_b64: welcomeB64
2935
+ }));
2936
+ const memberBody = {
2937
+ op: "add",
2938
+ commit_b64: toBase64(out.commit),
2939
+ target_device_ids: adds.map((a) => a.deviceId),
2940
+ leaf_indexes: out.addedLeafIndices,
2941
+ user_ids: userIds,
2942
+ key_package_refs: [],
2943
+ welcomes
2944
+ };
2945
+ return {
2946
+ method: "POST",
2947
+ path: MessagingPaths.groupMembers(group.displayId),
2948
+ body: memberBody
2949
+ };
2950
+ });
2951
+ }
2952
+ /** Remove a user = evict ALL their device leaves in ONE commit (SP-1.6 D4). */
2953
+ async removeMember(group, userId) {
2954
+ const members = await this.listMembers(group.displayId);
2955
+ const deviceIds = members.filter((m) => m.user_id === userId).map((m) => m.device_id);
2956
+ if (deviceIds.length === 0) {
2957
+ throw new BackendError("validation", {
2958
+ code: "not_a_member",
2959
+ message: "user has no device in this group",
2960
+ status: 404
2961
+ });
2962
+ }
2963
+ await this.stageMultiRemove(group, deviceIds, [userId]);
2964
+ }
2965
+ /** Leave = remove the CALLING device's leaf only (per-device self-leave). */
2966
+ async leaveGroup(group) {
2967
+ await this.stageMultiRemove(group, [this.selfDeviceId], [this.selfUserId]);
2968
+ }
2969
+ async stageMultiRemove(group, deviceIds, removedUserIds) {
2970
+ const pathDeviceId = deviceIds[0];
2971
+ if (!pathDeviceId) throw new Error("no device to remove");
2972
+ await this.commitWithRebase(group.rfcGroupId, async () => {
2973
+ const out = await this.engine.commitChanges(
2974
+ fromBase64(group.rfcGroupId),
2975
+ [],
2976
+ deviceIds.map(deviceIdBytes)
2977
+ );
2978
+ const body = {
2979
+ op: "remove",
2980
+ commit_b64: toBase64(out.commit),
2981
+ target_device_ids: deviceIds,
2982
+ user_ids: removedUserIds
2983
+ };
2984
+ return {
2985
+ method: "DELETE",
2986
+ path: MessagingPaths.groupMemberDevice(group.displayId, pathDeviceId),
2987
+ body
2988
+ };
2989
+ });
2990
+ }
2991
+ // ── Send ──
2992
+ /** Send a text message. Encrypt at the current epoch, POST, return the receipt +
2993
+ * the client-minted `clientMsgId` (needed by the Chat layer for reply indexing).
2994
+ * NEVER rebases (a 422 stale_application surfaces to the caller). */
2995
+ async sendText(group, text, replyTo) {
2996
+ const clientMsgId = mintClientMsgId();
2997
+ const plaintext = encodeEnvelope({ text, clientMsgId, replyTo });
2998
+ const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
2999
+ const body = {
3000
+ ciphertext_b64: toBase64(ct),
3001
+ client_idem_key: randomId()
3002
+ };
3003
+ const wire = await palbeRequest(
3004
+ this.rt,
3005
+ "POST",
3006
+ MessagingPaths.groupMessages(group.displayId),
3007
+ { body }
3008
+ );
3009
+ const stored = {
3010
+ id: `${group.rfcGroupId}#${wire.server_seq}`,
3011
+ direction: "outgoing",
3012
+ text,
3013
+ senderDeviceId: this.selfDeviceId,
3014
+ epoch: wire.epoch,
3015
+ serverSeq: wire.server_seq,
3016
+ at: Date.now(),
3017
+ clientMsgId,
3018
+ replyTo: replyTo ? {
3019
+ clientMsgId: replyTo.client_msg_id,
3020
+ previewBody: replyTo.preview?.body ?? null,
3021
+ previewAuthorUserId: replyTo.preview?.author_user_id ?? null,
3022
+ previewKind: replyTo.preview?.kind ?? "text"
3023
+ } : null
3024
+ };
3025
+ try {
3026
+ await this.messageStore.append(group.rfcGroupId, stored);
3027
+ } catch {
3028
+ }
3029
+ return { receipt: { serverSeq: wire.server_seq, epoch: wire.epoch }, clientMsgId };
3030
+ }
3031
+ // ── The rebase loop ──
3032
+ async commitWithRebase(rfcGroupId, build) {
3033
+ const gidBytes = fromBase64(rfcGroupId);
3034
+ let lastEpoch = null;
3035
+ for (let attempt = 1; attempt <= this.rebase.maxAttempts; attempt++) {
3036
+ const pending = await build();
3037
+ try {
3038
+ await palbeRequest(this.rt, pending.method, pending.path, {
3039
+ body: pending.body
3040
+ });
3041
+ await this.engine.applyPendingCommit(gidBytes);
3042
+ return;
3043
+ } catch (e) {
3044
+ if (isBackendError(e) && e.code === "epoch_conflict") {
3045
+ await this.engine.clearPendingCommit(gidBytes);
3046
+ const ce = e.data?.current_epoch;
3047
+ if (typeof ce === "number") lastEpoch = ce;
3048
+ if (attempt === this.rebase.maxAttempts) break;
3049
+ await this.drain(rfcGroupId);
3050
+ await this.backoff(attempt);
3051
+ continue;
3052
+ }
3053
+ throw e;
3054
+ }
3055
+ }
3056
+ await this.engine.clearPendingCommit(gidBytes);
3057
+ throw new RebaseExhausted(this.rebase.maxAttempts, lastEpoch);
3058
+ }
3059
+ /** Pull the device queue, process the winning commit(s) so the engine advances
3060
+ * to the new epoch, ack the commits (the delivery source owns app messages). */
3061
+ async drain(rfcGroupId) {
3062
+ const page = await palbeRequest(
3063
+ this.rt,
3064
+ "GET",
3065
+ MessagingPaths.deviceQueue(this.selfDeviceId)
3066
+ );
3067
+ const gidBytes = fromBase64(rfcGroupId);
3068
+ const ackIds = [];
3069
+ for (const row of page.messages) {
3070
+ if (row.content_type !== ContentType.commit) continue;
3071
+ try {
3072
+ await this.engine.processIncoming(gidBytes, fromBase64(row.ciphertext_b64));
3073
+ } catch {
3074
+ }
3075
+ ackIds.push(row.id);
3076
+ }
3077
+ if (ackIds.length > 0) {
3078
+ await palbeRequest(this.rt, "POST", MessagingPaths.deviceQueueAck(this.selfDeviceId), {
3079
+ body: { message_ids: ackIds }
3080
+ });
3081
+ }
3082
+ }
3083
+ async backoff(attempt) {
3084
+ const base = this.rebase.baseDelayMs * attempt;
3085
+ const jitter = this.rebase.jitterMs === 0 ? 0 : Math.random() * this.rebase.jitterMs;
3086
+ const total = base + jitter;
3087
+ if (total <= 0) return;
3088
+ await new Promise((r) => setTimeout(r, total));
3089
+ }
3090
+ };
3091
+
3092
+ // src/messaging/chat.ts
3093
+ var Chat = class {
3094
+ /** Stable, URL/log-safe id (the grp_ display id once active; a reserved local id while draft). */
3095
+ id;
3096
+ kind;
3097
+ _state;
3098
+ _group;
3099
+ _draft;
3100
+ backend;
3101
+ messageList = [];
3102
+ memberCache = [];
3103
+ typingList = [];
3104
+ presenceStates = /* @__PURE__ */ new Map();
3105
+ readWatermark = -1;
3106
+ titleOverride = null;
3107
+ seenKeys = /* @__PURE__ */ new Set();
3108
+ /** Index: clientMsgId → { text, senderUserId } for resolveReply lookups. */
3109
+ byClientMsgId = /* @__PURE__ */ new Map();
3110
+ loadedEarliestSeq = null;
3111
+ historyLoaded = false;
3112
+ wired = false;
3113
+ liveUnsub = null;
3114
+ listeners = /* @__PURE__ */ new Set();
3115
+ /** @internal — obtain via pb.messaging.directChat / groupChat / chat(id). */
3116
+ constructor(args) {
3117
+ this.backend = args.backend;
3118
+ if ("group" in args) {
3119
+ this.id = args.group.displayId;
3120
+ this.kind = args.kind;
3121
+ this._state = "active";
3122
+ this._group = args.group;
3123
+ this._draft = null;
3124
+ this.seedMembersFromGroup(args.group);
3125
+ } else {
3126
+ this.id = args.draft.reservedId;
3127
+ this.kind = args.kind;
3128
+ this._state = "draft";
3129
+ this._group = null;
3130
+ this._draft = args.draft;
3131
+ this.seedDraftMembers(args.draft);
3132
+ }
3133
+ }
3134
+ // ── Observability ──
3135
+ /** Subscribe to any change in this chat (messages/members/typing/state). */
3136
+ onChange(cb) {
3137
+ this.listeners.add(cb);
3138
+ this.ensureWired();
3139
+ return () => this.listeners.delete(cb);
3140
+ }
3141
+ emit() {
3142
+ for (const cb of this.listeners) cb();
3143
+ }
3144
+ // ── Snapshot getters ──
3145
+ get state() {
3146
+ return this._state;
3147
+ }
3148
+ get isDirect() {
3149
+ return this.kind === "direct";
3150
+ }
3151
+ get messages() {
3152
+ return this.messageList;
3153
+ }
3154
+ get members() {
3155
+ return this.memberCache;
3156
+ }
3157
+ get typing() {
3158
+ return this.typingList;
3159
+ }
3160
+ get lastMessage() {
3161
+ return this.messageList.at(-1) ?? null;
3162
+ }
3163
+ get unreadCount() {
3164
+ return this.messageList.filter(
3165
+ (m) => m.direction === "incoming" && m.serverSeq > this.readWatermark
3166
+ ).length;
3167
+ }
3168
+ get title() {
3169
+ if (this.titleOverride) return this.titleOverride;
3170
+ if (this._group?.name) return this._group.name;
3171
+ if (this._draft) {
3172
+ const mode = this._draft.mode;
3173
+ if (mode.kind === "direct") {
3174
+ const peer = this.memberCache.find((m) => m.userId === mode.peerUserId);
3175
+ return peer?.displayName ?? "Chat";
3176
+ }
3177
+ return "Group";
3178
+ }
3179
+ const tail = this.id.startsWith("grp_") ? this.id.slice(4) : this.id;
3180
+ const short = tail.slice(-4);
3181
+ return short ? `Group ${short}` : "Group";
3182
+ }
3183
+ presence(userId) {
3184
+ return this.presenceStates.get(userId) ?? null;
3185
+ }
3186
+ // ── Wiring (live + history) ──
3187
+ ensureWired() {
3188
+ if (this.wired || this._state !== "active" || !this._group) return;
3189
+ this.wired = true;
3190
+ this.liveUnsub = this.backend.subscribeLive(this._group, this);
3191
+ void this.hydrateHistory();
3192
+ void this.refreshMembers();
3193
+ }
3194
+ async hydrateHistory() {
3195
+ if (this.historyLoaded || !this._group) return;
3196
+ this.historyLoaded = true;
3197
+ const entries = await this.backend.history(this._group, 50);
3198
+ this.mergeHistory(entries);
3199
+ }
3200
+ mergeHistory(incoming) {
3201
+ let changed = false;
3202
+ for (const m of incoming) {
3203
+ if (m.serverSeq <= 0) continue;
3204
+ if (m.clientMsgId && m.text !== null) {
3205
+ this.byClientMsgId.set(m.clientMsgId, {
3206
+ text: m.text,
3207
+ senderUserId: m.senderUserId ?? ""
3208
+ });
3209
+ }
3210
+ }
3211
+ for (const m of incoming) {
3212
+ if (m.serverSeq <= 0) continue;
3213
+ const key = this.internalKey(m.serverSeq);
3214
+ if (this.seenKeys.has(key)) continue;
3215
+ this.seenKeys.add(key);
3216
+ this.messageList.push(m);
3217
+ changed = true;
3218
+ this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
3219
+ }
3220
+ if (changed) {
3221
+ this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
3222
+ this.emit();
3223
+ }
3224
+ }
3225
+ /** @internal — called by the backend's live subscription. */
3226
+ async ingestLive(incoming) {
3227
+ if (!this._group) return;
3228
+ if (incoming.kind === "commit") {
3229
+ await this.refreshMembers();
3230
+ return;
3231
+ }
3232
+ if (incoming.serverSeq <= 0) return;
3233
+ const key = this.internalKey(incoming.serverSeq);
3234
+ if (this.seenKeys.has(key)) return;
3235
+ this.seenKeys.add(key);
3236
+ let senderUser = null;
3237
+ if (incoming.senderDeviceId) {
3238
+ senderUser = await this.backend.userIdForDevice(this._group, incoming.senderDeviceId);
3239
+ }
3240
+ const direction = senderUser !== null && senderUser === this.backend.selfUserId ? "outgoing" : "incoming";
3241
+ const incomingClientMsgId = incoming.clientMsgId;
3242
+ const incomingReplyRef = incoming.replyRef;
3243
+ let resolvedReplyTo = null;
3244
+ if (incomingReplyRef) {
3245
+ resolvedReplyTo = resolveReply(incomingReplyRef, (id) => this.byClientMsgId.get(id) ?? null);
3246
+ }
3247
+ const msg = {
3248
+ id: this.publicId(incoming.serverSeq),
3249
+ kind: this.kindOf(incoming),
3250
+ direction,
3251
+ senderUserId: senderUser,
3252
+ text: incoming.text,
3253
+ serverSeq: incoming.serverSeq,
3254
+ sentAt: incoming.receivedAt,
3255
+ clientMsgId: incomingClientMsgId,
3256
+ replyTo: resolvedReplyTo
3257
+ };
3258
+ if (incomingClientMsgId && incoming.text !== null) {
3259
+ this.byClientMsgId.set(incomingClientMsgId, {
3260
+ text: incoming.text,
3261
+ senderUserId: senderUser ?? ""
3262
+ });
3263
+ }
3264
+ this.messageList.push(msg);
3265
+ this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
3266
+ this.loadedEarliestSeq = Math.min(
3267
+ this.loadedEarliestSeq ?? incoming.serverSeq,
3268
+ incoming.serverSeq
3269
+ );
3270
+ this.emit();
3271
+ }
3272
+ /** @internal — called by the backend's conv subscription. */
3273
+ applyConv(event, payload) {
3274
+ const userId = typeof payload.user_id === "string" ? payload.user_id : null;
3275
+ if (!userId || userId === this.backend.selfUserId) return;
3276
+ if (event === "typing") {
3277
+ const isTyping = payload.typing === true;
3278
+ const member = this.memberCache.find((m) => m.userId === userId) ?? {
3279
+ id: userId,
3280
+ userId,
3281
+ displayName: null,
3282
+ role: "member",
3283
+ isSelf: false
3284
+ };
3285
+ this.typingList = isTyping ? [...this.typingList.filter((m) => m.userId !== userId), member] : this.typingList.filter((m) => m.userId !== userId);
3286
+ this.emit();
3287
+ } else if (event === "presence") {
3288
+ const online = payload.online === true;
3289
+ const ts = typeof payload.ts === "number" ? payload.ts : null;
3290
+ this.presenceStates.set(userId, {
3291
+ userId,
3292
+ isOnline: online,
3293
+ lastSeenAt: ts ? new Date(ts * 1e3) : null
3294
+ });
3295
+ this.emit();
3296
+ }
3297
+ }
3298
+ kindOf(incoming) {
3299
+ if (incoming.kind === "commit" || incoming.kind === "proposal" || incoming.kind === "welcome")
3300
+ return "system";
3301
+ return "text";
3302
+ }
3303
+ internalKey(serverSeq) {
3304
+ return this._group ? `${this._group.rfcGroupId}#${serverSeq}` : `draft#${serverSeq}`;
3305
+ }
3306
+ publicId(serverSeq) {
3307
+ return `${this.id}#${serverSeq}`;
3308
+ }
3309
+ /** Page older messages in. Returns how many were prepended. */
3310
+ async loadEarlier(limit = 50) {
3311
+ if (!this._group) return 0;
3312
+ const before = this.loadedEarliestSeq ?? void 0;
3313
+ const older = await this.backend.history(this._group, limit, before);
3314
+ const before0 = this.messageList.length;
3315
+ this.mergeHistory(older);
3316
+ return this.messageList.length - before0;
3317
+ }
3318
+ // ── Members ──
3319
+ async refreshMembers() {
3320
+ if (!this._group) return;
3321
+ const m = await this.backend.members(this._group);
3322
+ if (m.length > 0) {
3323
+ this.memberCache = m;
3324
+ this.emit();
3325
+ }
3326
+ }
3327
+ seedMembersFromGroup(group) {
3328
+ const seed = [
3329
+ {
3330
+ id: this.backend.selfUserId,
3331
+ userId: this.backend.selfUserId,
3332
+ displayName: null,
3333
+ role: "owner",
3334
+ isSelf: true
3335
+ }
3336
+ ];
3337
+ if (group.ownerUserId && group.ownerUserId !== this.backend.selfUserId) {
3338
+ seed.push({
3339
+ id: group.ownerUserId,
3340
+ userId: group.ownerUserId,
3341
+ displayName: null,
3342
+ role: "owner",
3343
+ isSelf: false
3344
+ });
3345
+ }
3346
+ this.memberCache = seed;
3347
+ }
3348
+ seedDraftMembers(draft) {
3349
+ const seed = [
3350
+ {
3351
+ id: this.backend.selfUserId,
3352
+ userId: this.backend.selfUserId,
3353
+ displayName: null,
3354
+ role: draft.mode.kind === "group" ? "owner" : "member",
3355
+ isSelf: true
3356
+ }
3357
+ ];
3358
+ if (draft.mode.kind === "direct") {
3359
+ seed.push({
3360
+ id: draft.mode.peerUserId,
3361
+ userId: draft.mode.peerUserId,
3362
+ displayName: null,
3363
+ role: "member",
3364
+ isSelf: false
3365
+ });
3366
+ } else {
3367
+ for (const uid of draft.mode.members) {
3368
+ seed.push({ id: uid, userId: uid, displayName: null, role: "member", isSelf: false });
3369
+ }
3370
+ }
3371
+ this.memberCache = seed;
3372
+ }
3373
+ // ── Send (with WhatsApp lazy materialize) ──
3374
+ async send(text, opts) {
3375
+ const group = await this.materializeIfNeeded();
3376
+ let replyRef = null;
3377
+ let resolvedReplyTo = null;
3378
+ const parent = opts?.replyTo;
3379
+ if (parent?.clientMsgId && parent.kind !== "system") {
3380
+ const [previewBody, wasTruncated] = parent.text ? truncateQuote(parent.text) : ["", false];
3381
+ replyRef = {
3382
+ v: 1,
3383
+ client_msg_id: parent.clientMsgId,
3384
+ preview: {
3385
+ kind: "text",
3386
+ author_user_id: parent.senderUserId ?? "",
3387
+ body: previewBody || void 0,
3388
+ body_truncated: wasTruncated
3389
+ }
3390
+ };
3391
+ resolvedReplyTo = resolveReply(replyRef, (id) => this.byClientMsgId.get(id) ?? null);
3392
+ }
3393
+ const { receipt, clientMsgId } = await this.backend.sendText(group, text, replyRef);
3394
+ this.appendOwnSend(text, receipt, clientMsgId, resolvedReplyTo);
3395
+ return receipt;
3396
+ }
3397
+ appendOwnSend(text, receipt, clientMsgId, resolvedReplyTo) {
3398
+ if (receipt.serverSeq <= 0) return;
3399
+ const key = this.internalKey(receipt.serverSeq);
3400
+ if (this.seenKeys.has(key)) return;
3401
+ this.seenKeys.add(key);
3402
+ if (clientMsgId) {
3403
+ this.byClientMsgId.set(clientMsgId, { text, senderUserId: this.backend.selfUserId });
3404
+ }
3405
+ this.messageList.push({
3406
+ id: this.publicId(receipt.serverSeq),
3407
+ kind: "text",
3408
+ direction: "outgoing",
3409
+ senderUserId: this.backend.selfUserId,
3410
+ text,
3411
+ serverSeq: receipt.serverSeq,
3412
+ sentAt: /* @__PURE__ */ new Date(),
3413
+ clientMsgId,
3414
+ replyTo: resolvedReplyTo
3415
+ });
3416
+ this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
3417
+ this.emit();
3418
+ }
3419
+ async materializeIfNeeded() {
3420
+ if (this._group) return this._group;
3421
+ if (!this._draft) throw new Error("messaging_not_configured");
3422
+ const group = await this.backend.materialize(this._draft);
3423
+ this._group = group;
3424
+ this.id = group.displayId;
3425
+ this._state = "active";
3426
+ this._draft = null;
3427
+ this.backend.registerActive(group);
3428
+ this.ensureWired();
3429
+ this.emit();
3430
+ return group;
3431
+ }
3432
+ // ── Membership (groups only) ──
3433
+ async addMemberUser(userId) {
3434
+ if (this.kind !== "group") throw new Error("chat_is_direct");
3435
+ const group = await this.materializeIfNeeded();
3436
+ await this.backend.addMember(group, userId);
3437
+ await this.refreshMembers();
3438
+ }
3439
+ async removeMemberUser(userId) {
3440
+ if (this.kind !== "group") throw new Error("chat_is_direct");
3441
+ if (!this._group) throw new Error("messaging_not_configured");
3442
+ await this.backend.removeMember(this._group, userId);
3443
+ await this.refreshMembers();
3444
+ }
3445
+ async leave() {
3446
+ if (!this._group) return;
3447
+ await this.backend.leave(this._group);
3448
+ this.liveUnsub?.();
3449
+ this.liveUnsub = null;
3450
+ }
3451
+ // ── Imperative ──
3452
+ setTyping(isTyping) {
3453
+ if (this._group) this.backend.setTyping(this._group, isTyping);
3454
+ }
3455
+ async markRead(message) {
3456
+ if (!this._group) return;
3457
+ await this.backend.markRead(this._group, message.serverSeq);
3458
+ this.readWatermark = Math.max(this.readWatermark, message.serverSeq);
3459
+ this.emit();
3460
+ }
3461
+ };
3462
+
3463
+ // src/messaging/delivery-source.ts
3464
+ var MessageHub = class {
3465
+ messageListeners = /* @__PURE__ */ new Map();
3466
+ convListeners = /* @__PURE__ */ new Map();
3467
+ onMessage(displayId, fn) {
3468
+ return this.add(this.messageListeners, displayId, fn);
3469
+ }
3470
+ emitMessage(displayId, m) {
3471
+ for (const fn of this.messageListeners.get(displayId) ?? []) fn(m);
3472
+ }
3473
+ onConv(displayId, fn) {
3474
+ return this.add(this.convListeners, displayId, fn);
3475
+ }
3476
+ emitConv(displayId, e) {
3477
+ for (const fn of this.convListeners.get(displayId) ?? []) fn(e);
3478
+ }
3479
+ add(map, key, fn) {
3480
+ let set = map.get(key);
3481
+ if (!set) {
3482
+ set = /* @__PURE__ */ new Set();
3483
+ map.set(key, set);
3484
+ }
3485
+ set.add(fn);
3486
+ return () => {
3487
+ set?.delete(fn);
3488
+ };
3489
+ }
3490
+ };
3491
+ var MessageDeliverySource = class {
3492
+ constructor(rt, engine, hub, registry, messageStore, deviceId, selfUserId) {
3493
+ this.rt = rt;
3494
+ this.engine = engine;
3495
+ this.hub = hub;
3496
+ this.registry = registry;
3497
+ this.messageStore = messageStore;
3498
+ this.deviceId = deviceId;
3499
+ this.selfUserId = selfUserId;
3500
+ }
3501
+ rt;
3502
+ engine;
3503
+ hub;
3504
+ registry;
3505
+ messageStore;
3506
+ deviceId;
3507
+ selfUserId;
3508
+ started = false;
3509
+ wakeUnsub = null;
3510
+ // Per-observed-group conv subscription + heartbeat (presence/typing/read).
3511
+ observed = /* @__PURE__ */ new Map();
3512
+ /** Subscribe the device wake topic + run an initial drain. Idempotent. */
3513
+ async start() {
3514
+ if (this.started) return;
3515
+ this.started = true;
3516
+ try {
3517
+ const channel = this.rt.realtime.channel(`messaging:device:${this.deviceId}`);
3518
+ const sub = channel.on("new_message", () => {
3519
+ void this.pumpOnce();
3520
+ });
3521
+ const offConn = this.rt.realtime.status.onChange((s) => {
3522
+ if (s.state === "connected") void this.pumpOnce();
3523
+ });
3524
+ this.wakeUnsub = () => {
3525
+ sub.cancel();
3526
+ offConn();
3527
+ };
3528
+ } catch {
3529
+ }
3530
+ await this.pumpOnce();
3531
+ }
3532
+ stop() {
3533
+ this.wakeUnsub?.();
3534
+ this.wakeUnsub = null;
3535
+ for (const [, o] of this.observed) {
3536
+ o.unsub();
3537
+ if (o.heartbeat) clearInterval(o.heartbeat);
3538
+ }
3539
+ this.observed.clear();
3540
+ this.started = false;
3541
+ }
3542
+ /** One full drain: welcomes → queue (decrypt + emit) → ack. Never throws. */
3543
+ async pumpOnce() {
3544
+ try {
3545
+ await this.pullWelcomes();
3546
+ } catch {
3547
+ }
3548
+ try {
3549
+ await this.pullQueue();
3550
+ } catch {
3551
+ }
3552
+ }
3553
+ // ── Welcomes ──
3554
+ async pullWelcomes() {
3555
+ const page = await palbeRequest(
3556
+ this.rt,
3557
+ "GET",
3558
+ MessagingPaths.deviceWelcomes(this.deviceId)
3559
+ );
3560
+ for (const w of page.welcomes) {
3561
+ try {
3562
+ const gidBytes = await this.engine.joinFromWelcome(fromBase64(w.welcome_b64));
3563
+ const group = {
3564
+ displayId: w.display_id,
3565
+ rfcGroupId: toBase64(gidBytes),
3566
+ currentEpoch: w.added_at_epoch,
3567
+ ownerUserId: null,
3568
+ directKey: null,
3569
+ name: null
3570
+ };
3571
+ this.registry.register(group);
3572
+ this.hub.emitMessage(group.displayId, {
3573
+ kind: "welcome",
3574
+ group,
3575
+ text: null,
3576
+ senderDeviceId: null,
3577
+ epoch: w.added_at_epoch,
3578
+ serverSeq: 0,
3579
+ receivedAt: /* @__PURE__ */ new Date(),
3580
+ clientMsgId: "",
3581
+ replyRef: null
3582
+ });
3583
+ } catch {
3584
+ const known = this.registry.all().some((g) => g.displayId === w.display_id);
3585
+ if (!known) {
3586
+ }
3587
+ }
3588
+ }
3589
+ }
3590
+ // ── Queue ──
3591
+ async pullQueue() {
3592
+ const page = await palbeRequest(
3593
+ this.rt,
3594
+ "GET",
3595
+ MessagingPaths.deviceQueue(this.deviceId)
3596
+ );
3597
+ const ackIds = [];
3598
+ for (const row of page.messages) {
3599
+ if (await this.process(row)) ackIds.push(row.id);
3600
+ }
3601
+ if (ackIds.length > 0) {
3602
+ await palbeRequest(this.rt, "POST", MessagingPaths.deviceQueueAck(this.deviceId), {
3603
+ body: { message_ids: ackIds }
3604
+ });
3605
+ }
3606
+ }
3607
+ /** Process one queue row; returns true if consumed (should ack). */
3608
+ async process(row) {
3609
+ let blob;
3610
+ try {
3611
+ blob = fromBase64(row.ciphertext_b64);
3612
+ } catch {
3613
+ return true;
3614
+ }
3615
+ const group = (row.rfc_group_id_b64 ? this.registry.group(row.rfc_group_id_b64) : void 0) ?? this.registry.soleGroup();
3616
+ if (!group) return false;
3617
+ try {
3618
+ const received = await this.engine.processIncoming(fromBase64(group.rfcGroupId), blob);
3619
+ if (received.type === "application") {
3620
+ const { text, clientMsgId, replyTo } = decodeEnvelope(received.data);
3621
+ const stored = {
3622
+ id: `${group.rfcGroupId}#${row.server_seq}`,
3623
+ direction: "incoming",
3624
+ text,
3625
+ senderDeviceId: row.sender_device_id ?? null,
3626
+ epoch: row.epoch,
3627
+ serverSeq: row.server_seq,
3628
+ at: Date.now(),
3629
+ clientMsgId: clientMsgId || void 0,
3630
+ replyTo: replyTo ? {
3631
+ clientMsgId: replyTo.client_msg_id,
3632
+ previewBody: replyTo.preview?.body ?? null,
3633
+ previewAuthorUserId: replyTo.preview?.author_user_id ?? null,
3634
+ previewKind: replyTo.preview?.kind ?? "text"
3635
+ } : null
3636
+ };
3637
+ try {
3638
+ await this.messageStore.append(group.rfcGroupId, stored);
3639
+ } catch {
3640
+ return false;
3641
+ }
3642
+ this.hub.emitMessage(group.displayId, {
3643
+ kind: "application",
3644
+ group,
3645
+ text,
3646
+ senderDeviceId: row.sender_device_id ?? null,
3647
+ epoch: row.epoch,
3648
+ serverSeq: row.server_seq,
3649
+ receivedAt: /* @__PURE__ */ new Date(),
3650
+ clientMsgId,
3651
+ replyRef: replyTo
3652
+ });
3653
+ return true;
3654
+ }
3655
+ if (received.type === "commitApplied") {
3656
+ const newEpoch = Math.max(row.epoch, group.currentEpoch + 1);
3657
+ this.registry.bumpEpoch(group.rfcGroupId, newEpoch);
3658
+ const fresh = this.registry.group(group.rfcGroupId) ?? group;
3659
+ this.hub.emitMessage(group.displayId, {
3660
+ kind: "commit",
3661
+ group: fresh,
3662
+ text: null,
3663
+ senderDeviceId: row.sender_device_id ?? null,
3664
+ epoch: newEpoch,
3665
+ serverSeq: 0,
3666
+ receivedAt: /* @__PURE__ */ new Date(),
3667
+ clientMsgId: "",
3668
+ replyRef: null
3669
+ });
3670
+ return true;
3671
+ }
3672
+ return true;
3673
+ } catch (e) {
3674
+ if (isOwnEchoOrConsumed(e)) return true;
3675
+ return false;
3676
+ }
3677
+ }
3678
+ // ── Conv topic (presence / typing / read) ──
3679
+ /** Begin observing a group's conv topic; fans events into the hub. Idempotent. */
3680
+ observeConversation(group) {
3681
+ if (this.observed.has(group.displayId)) return;
3682
+ try {
3683
+ const channel = this.rt.realtime.channel(`messaging:conv:${group.rfcGroupId}`);
3684
+ const subs = ["presence", "typing", "read"].map(
3685
+ (ev) => channel.on(ev, (payload) => {
3686
+ this.hub.emitConv(group.displayId, { event: ev, payload });
3687
+ })
3688
+ );
3689
+ const emitPresence = (online) => channel.send("presence", {
3690
+ user_id: this.selfUserId,
3691
+ device_id: this.deviceId,
3692
+ online,
3693
+ ts: Date.now() / 1e3
3694
+ });
3695
+ emitPresence(true);
3696
+ const heartbeat = setInterval(() => emitPresence(true), 25e3);
3697
+ this.observed.set(group.displayId, {
3698
+ unsub: () => {
3699
+ for (const s of subs) s.cancel();
3700
+ },
3701
+ heartbeat
3702
+ });
3703
+ } catch {
3704
+ }
3705
+ }
3706
+ /** Announce typing on a group's conv topic (the app calls per keystroke). */
3707
+ setTyping(group, isTyping) {
3708
+ this.observeConversation(group);
3709
+ try {
3710
+ this.rt.realtime.channel(`messaging:conv:${group.rfcGroupId}`).send("typing", {
3711
+ user_id: this.selfUserId,
3712
+ device_id: this.deviceId,
3713
+ typing: isTyping
3714
+ });
3715
+ } catch {
3716
+ }
3717
+ }
3718
+ /** Advance this device's read cursor (idempotent; the server clamps). */
3719
+ async markRead(group, upToServerSeq) {
3720
+ await palbeRequest(this.rt, "POST", MessagingPaths.groupRead(group.displayId), {
3721
+ body: { read_seq: upToServerSeq, read_epoch: group.currentEpoch, is_private: false }
3722
+ });
3723
+ }
3724
+ };
3725
+ function isOwnEchoOrConsumed(e) {
3726
+ const msg = e instanceof Error ? e.message : String(e);
3727
+ return msg.includes("message from self") || msg.includes("key not available, invalid generation");
3728
+ }
3729
+
3730
+ // src/messaging/history.ts
3731
+ var MessageStore = class {
3732
+ constructor(kv) {
3733
+ this.kv = kv;
3734
+ }
3735
+ kv;
3736
+ key(rfcGroupId) {
3737
+ return `msgs:${rfcGroupId}`;
3738
+ }
3739
+ /** Append (idempotent on id) and persist the group's transcript. */
3740
+ async append(rfcGroupId, msg) {
3741
+ const list = await this.load(rfcGroupId);
3742
+ if (list.some((m) => m.id === msg.id)) return;
3743
+ list.push(msg);
3744
+ list.sort((a, b) => a.serverSeq - b.serverSeq);
3745
+ await this.save(rfcGroupId, list);
3746
+ }
3747
+ /** True if a message id is already durable (the consumed-key recovery check). */
3748
+ async contains(rfcGroupId, id) {
3749
+ const list = await this.load(rfcGroupId);
3750
+ return list.some((m) => m.id === id);
3751
+ }
3752
+ /**
3753
+ * Read history for a group, newest-last. `before` pages older: returns up to
3754
+ * `limit` rows with serverSeq < before. nil `before` returns the newest page.
3755
+ */
3756
+ async history(rfcGroupId, limit, before) {
3757
+ const list = await this.load(rfcGroupId);
3758
+ const filtered = before == null ? list : list.filter((m) => m.serverSeq < before);
3759
+ return filtered.slice(Math.max(0, filtered.length - limit));
3760
+ }
3761
+ async load(rfcGroupId) {
3762
+ const raw = await this.kv.get(this.key(rfcGroupId));
3763
+ if (!raw) return [];
3764
+ try {
3765
+ return JSON.parse(decodeUtf8(raw));
3766
+ } catch {
3767
+ return [];
3768
+ }
3769
+ }
3770
+ async save(rfcGroupId, list) {
3771
+ await this.kv.set(this.key(rfcGroupId), encodeUtf8(JSON.stringify(list)));
3772
+ }
3773
+ async wipe() {
3774
+ for (const k of await this.kv.keys("msgs:")) await this.kv.delete(k);
3775
+ }
3776
+ };
3777
+ var GroupCatalog = class {
3778
+ constructor(kv) {
3779
+ this.kv = kv;
3780
+ }
3781
+ kv;
3782
+ async upsert(g) {
3783
+ await this.kv.set(`cat:${g.rfcGroupId}`, encodeUtf8(JSON.stringify(g)));
3784
+ }
3785
+ async remove(rfcGroupId) {
3786
+ await this.kv.delete(`cat:${rfcGroupId}`);
3787
+ }
3788
+ async all() {
3789
+ const keys = await this.kv.keys("cat:");
3790
+ const out = [];
3791
+ for (const k of keys) {
3792
+ const raw = await this.kv.get(k);
3793
+ if (!raw) continue;
3794
+ try {
3795
+ out.push(JSON.parse(decodeUtf8(raw)));
3796
+ } catch {
3797
+ }
3798
+ }
3799
+ return out;
3800
+ }
3801
+ async wipe() {
3802
+ for (const k of await this.kv.keys("cat:")) await this.kv.delete(k);
3803
+ }
3804
+ };
3805
+
3806
+ // src/messaging/wasm/pkg/palbe_mls_bg.js
3807
+ var palbe_mls_bg_exports = {};
3808
+ __export(palbe_mls_bg_exports, {
3809
+ MlsClient: () => MlsClient,
3810
+ MlsGroup: () => MlsGroup,
3811
+ __wbg_BigInt_231999d28f899902: () => __wbg_BigInt_231999d28f899902,
3812
+ __wbg_Error_fdd633d4bb5dd76a: () => __wbg_Error_fdd633d4bb5dd76a,
3813
+ __wbg_String_8564e559799eccda: () => __wbg_String_8564e559799eccda,
3814
+ __wbg___wbindgen_bigint_get_as_i64_d9e915702856f831: () => __wbg___wbindgen_bigint_get_as_i64_d9e915702856f831,
3815
+ __wbg___wbindgen_debug_string_8a447059637473e2: () => __wbg___wbindgen_debug_string_8a447059637473e2,
3816
+ __wbg___wbindgen_is_function_acc5528be2b923f2: () => __wbg___wbindgen_is_function_acc5528be2b923f2,
3817
+ __wbg___wbindgen_is_null_6d937fbfb6478470: () => __wbg___wbindgen_is_null_6d937fbfb6478470,
3818
+ __wbg___wbindgen_is_object_0beba4a1980d3eea: () => __wbg___wbindgen_is_object_0beba4a1980d3eea,
3819
+ __wbg___wbindgen_is_string_1fca8072260dd261: () => __wbg___wbindgen_is_string_1fca8072260dd261,
3820
+ __wbg___wbindgen_is_undefined_721f8decd50c87a3: () => __wbg___wbindgen_is_undefined_721f8decd50c87a3,
3821
+ __wbg___wbindgen_jsval_eq_4e8c38722cb8ff51: () => __wbg___wbindgen_jsval_eq_4e8c38722cb8ff51,
3822
+ __wbg___wbindgen_memory_9751d9a3017e7c25: () => __wbg___wbindgen_memory_9751d9a3017e7c25,
3823
+ __wbg___wbindgen_number_get_1cc01dd708740256: () => __wbg___wbindgen_number_get_1cc01dd708740256,
3824
+ __wbg___wbindgen_string_get_71bb4348194e31f0: () => __wbg___wbindgen_string_get_71bb4348194e31f0,
3825
+ __wbg___wbindgen_throw_ea4887a5f8f9a9db: () => __wbg___wbindgen_throw_ea4887a5f8f9a9db,
3826
+ __wbg_buffer_7447b9cc2267a9e5: () => __wbg_buffer_7447b9cc2267a9e5,
3827
+ __wbg_call_67f43c91d09298f2: () => __wbg_call_67f43c91d09298f2,
3828
+ __wbg_call_b51415974987aa44: () => __wbg_call_b51415974987aa44,
3829
+ __wbg_crypto_38df2bab126b63dc: () => __wbg_crypto_38df2bab126b63dc,
3830
+ __wbg_delete_21fafcbc7bb82c85: () => __wbg_delete_21fafcbc7bb82c85,
3831
+ __wbg_epoch_15c000ffe3004541: () => __wbg_epoch_15c000ffe3004541,
3832
+ __wbg_error_a6fa202b58aa1cd3: () => __wbg_error_a6fa202b58aa1cd3,
3833
+ __wbg_getRandomValues_c44a50d8cfdaebeb: () => __wbg_getRandomValues_c44a50d8cfdaebeb,
3834
+ __wbg_get_615446055a48103f: () => __wbg_get_615446055a48103f,
3835
+ __wbg_globalThis_6d268067835e6709: () => __wbg_globalThis_6d268067835e6709,
3836
+ __wbg_global_3fe6c6c8ad6e6fb2: () => __wbg_global_3fe6c6c8ad6e6fb2,
3837
+ __wbg_insert_2d611f2bf9b60fee: () => __wbg_insert_2d611f2bf9b60fee,
3838
+ __wbg_instanceof_Error_38f854307ecab4ce: () => __wbg_instanceof_Error_38f854307ecab4ce,
3839
+ __wbg_length_c552db98817b9523: () => __wbg_length_c552db98817b9523,
3840
+ __wbg_maxEpochId_0b176768b2f352fb: () => __wbg_maxEpochId_0b176768b2f352fb,
3841
+ __wbg_message_a05fcf872473ffc5: () => __wbg_message_a05fcf872473ffc5,
3842
+ __wbg_msCrypto_bd5a034af96bcba6: () => __wbg_msCrypto_bd5a034af96bcba6,
3843
+ __wbg_new_227d7c05414eb861: () => __wbg_new_227d7c05414eb861,
3844
+ __wbg_new_32de5cbf49ca7dcb: () => __wbg_new_32de5cbf49ca7dcb,
3845
+ __wbg_new_364c96143b8f3496: () => __wbg_new_364c96143b8f3496,
3846
+ __wbg_new_b8b71ca8104fc178: () => __wbg_new_b8b71ca8104fc178,
3847
+ __wbg_new_d9762fd75876aafe: () => __wbg_new_d9762fd75876aafe,
3848
+ __wbg_new_no_args_4010ad257320fa4f: () => __wbg_new_no_args_4010ad257320fa4f,
3849
+ __wbg_new_with_byte_offset_and_length_8b21e3b1308deb48: () => __wbg_new_with_byte_offset_and_length_8b21e3b1308deb48,
3850
+ __wbg_new_with_length_5fdafe029be917a5: () => __wbg_new_with_length_5fdafe029be917a5,
3851
+ __wbg_node_84ea875411254db1: () => __wbg_node_84ea875411254db1,
3852
+ __wbg_process_44c7a14e11e9f69e: () => __wbg_process_44c7a14e11e9f69e,
3853
+ __wbg_push_1303ce035391aed3: () => __wbg_push_1303ce035391aed3,
3854
+ __wbg_randomFillSync_6c25eac9869eb53c: () => __wbg_randomFillSync_6c25eac9869eb53c,
3855
+ __wbg_require_b4edbdcf3e2a1ef0: () => __wbg_require_b4edbdcf3e2a1ef0,
3856
+ __wbg_self_1035a7cbd1b0d959: () => __wbg_self_1035a7cbd1b0d959,
3857
+ __wbg_set_047d1ea37bb67c19: () => __wbg_set_047d1ea37bb67c19,
3858
+ __wbg_set_1ddc4b8cd44d0da4: () => __wbg_set_1ddc4b8cd44d0da4,
3859
+ __wbg_set_28cba565792ec75f: () => __wbg_set_28cba565792ec75f,
3860
+ __wbg_set_6be42768c690e380: () => __wbg_set_6be42768c690e380,
3861
+ __wbg_set_name_b4c29a3a72ebbddc: () => __wbg_set_name_b4c29a3a72ebbddc,
3862
+ __wbg_set_wasm: () => __wbg_set_wasm,
3863
+ __wbg_stack_3b0d974bbf31e44f: () => __wbg_stack_3b0d974bbf31e44f,
3864
+ __wbg_state_bd20456c0f3efbc9: () => __wbg_state_bd20456c0f3efbc9,
3865
+ __wbg_subarray_e0162dcdea48eb3a: () => __wbg_subarray_e0162dcdea48eb3a,
3866
+ __wbg_versions_276b2795b1c6a219: () => __wbg_versions_276b2795b1c6a219,
3867
+ __wbg_window_9c17850b5e99c0ab: () => __wbg_window_9c17850b5e99c0ab,
3868
+ __wbg_write_56b1cf5bb0e780d6: () => __wbg_write_56b1cf5bb0e780d6,
3869
+ __wbindgen_cast_0000000000000001: () => __wbindgen_cast_0000000000000001,
3870
+ __wbindgen_cast_0000000000000002: () => __wbindgen_cast_0000000000000002,
3871
+ __wbindgen_cast_0000000000000003: () => __wbindgen_cast_0000000000000003,
3872
+ __wbindgen_init_externref_table: () => __wbindgen_init_externref_table,
3873
+ generateClient: () => generateClient,
3874
+ setPanicHook: () => setPanicHook
3875
+ });
3876
+ var MlsClient = class _MlsClient {
3877
+ static __wrap(ptr) {
3878
+ const obj = Object.create(_MlsClient.prototype);
3879
+ obj.__wbg_ptr = ptr;
3880
+ MlsClientFinalization.register(obj, obj.__wbg_ptr, obj);
3881
+ return obj;
3882
+ }
3883
+ __destroy_into_raw() {
3884
+ const ptr = this.__wbg_ptr;
3885
+ this.__wbg_ptr = 0;
3886
+ MlsClientFinalization.unregister(this);
3887
+ return ptr;
3888
+ }
3889
+ free() {
3890
+ const ptr = this.__destroy_into_raw();
3891
+ wasm.__wbg_mlsclient_free(ptr, 0);
3892
+ }
3893
+ /**
3894
+ * The MLS Capabilities wire bytes (the keydir's `capabilities`).
3895
+ * @returns {Uint8Array}
3896
+ */
3897
+ capabilities() {
3898
+ const ret = wasm.mlsclient_capabilities(this.__wbg_ptr);
3899
+ var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
3900
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
3901
+ return v1;
3902
+ }
3903
+ /**
3904
+ * Create a new group at epoch 0. `groupId` `undefined`/`null` lets mls-rs
3905
+ * mint a globally-unique id (the rfc_group_id routing key).
3906
+ * @param {Uint8Array | null} [group_id]
3907
+ * @returns {MlsGroup}
3908
+ */
3909
+ createGroup(group_id) {
3910
+ var ptr0 = isLikeNone(group_id) ? 0 : passArray8ToWasm0(group_id, wasm.__wbindgen_malloc);
3911
+ var len0 = WASM_VECTOR_LEN;
3912
+ const ret = wasm.mlsclient_createGroup(this.__wbg_ptr, ptr0, len0);
3913
+ if (ret[2]) {
3914
+ throw takeFromExternrefTable0(ret[1]);
3915
+ }
3916
+ return MlsGroup.__wrap(ret[0]);
3917
+ }
3918
+ /**
3919
+ * The MLS Credential wire bytes (the keydir's `credential`).
3920
+ * @returns {Uint8Array}
3921
+ */
3922
+ credential() {
3923
+ const ret = wasm.mlsclient_credential(this.__wbg_ptr);
3924
+ var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
3925
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
3926
+ return v1;
3927
+ }
3928
+ /**
3929
+ * The MLS-encoded enrollment material `{ signatureKey, credential,
3930
+ * capabilities }` (byte fields as arrays-of-numbers — wrap in Uint8Array as
3931
+ * needed). Base64 these into the SP-0 keydir enroll body.
3932
+ * @returns {any}
3933
+ */
3934
+ enrollmentMaterial() {
3935
+ const ret = wasm.mlsclient_enrollmentMaterial(this.__wbg_ptr);
3936
+ if (ret[2]) {
3937
+ throw takeFromExternrefTable0(ret[1]);
3938
+ }
3939
+ return takeFromExternrefTable0(ret[0]);
3940
+ }
3941
+ /**
3942
+ * Mint a one-use KeyPackage (RAW RFC 9420 §10 bytes) — uploaded to the
3943
+ * keydir, consumed by an adder's `addMember`.
3944
+ * @returns {Uint8Array}
3945
+ */
3946
+ generateKeyPackage() {
3947
+ const ret = wasm.mlsclient_generateKeyPackage(this.__wbg_ptr);
3948
+ if (ret[3]) {
3949
+ throw takeFromExternrefTable0(ret[2]);
3950
+ }
3951
+ var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
3952
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
3953
+ return v1;
3954
+ }
3955
+ /**
3956
+ * Join a group from a Welcome (opaque MLSMessage bytes).
3957
+ * @param {Uint8Array} welcome
3958
+ * @returns {MlsGroup}
3959
+ */
3960
+ joinGroup(welcome) {
3961
+ const ptr0 = passArray8ToWasm0(welcome, wasm.__wbindgen_malloc);
3962
+ const len0 = WASM_VECTOR_LEN;
3963
+ const ret = wasm.mlsclient_joinGroup(this.__wbg_ptr, ptr0, len0);
3964
+ if (ret[2]) {
3965
+ throw takeFromExternrefTable0(ret[1]);
3966
+ }
3967
+ return MlsGroup.__wrap(ret[0]);
3968
+ }
3969
+ /**
3970
+ * Re-hydrate a previously-persisted group by its `groupId` (restart-safe
3971
+ * reload of an already-joined group).
3972
+ * @param {Uint8Array} group_id
3973
+ * @returns {MlsGroup}
3974
+ */
3975
+ loadGroup(group_id) {
3976
+ const ptr0 = passArray8ToWasm0(group_id, wasm.__wbindgen_malloc);
3977
+ const len0 = WASM_VECTOR_LEN;
3978
+ const ret = wasm.mlsclient_loadGroup(this.__wbg_ptr, ptr0, len0);
3979
+ if (ret[2]) {
3980
+ throw takeFromExternrefTable0(ret[1]);
3981
+ }
3982
+ return MlsGroup.__wrap(ret[0]);
3983
+ }
3984
+ /**
3985
+ * The serialized STABLE signature keypair (secret-bearing). Persist sealed
3986
+ * and pass back as `signatureKeypair` on restart so the device keeps the
3987
+ * SAME MLS signing identity.
3988
+ * @returns {Uint8Array}
3989
+ */
3990
+ signatureKeypair() {
3991
+ const ret = wasm.mlsclient_signatureKeypair(this.__wbg_ptr);
3992
+ var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
3993
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
3994
+ return v1;
3995
+ }
3996
+ /**
3997
+ * The device's STABLE signature PUBLIC key bytes (the keydir's
3998
+ * `signature_key`).
3999
+ * @returns {Uint8Array}
4000
+ */
4001
+ signaturePublicKey() {
4002
+ const ret = wasm.mlsclient_signaturePublicKey(this.__wbg_ptr);
4003
+ var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
4004
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
4005
+ return v1;
4006
+ }
4007
+ };
4008
+ if (Symbol.dispose) MlsClient.prototype[Symbol.dispose] = MlsClient.prototype.free;
4009
+ var MlsGroup = class _MlsGroup {
4010
+ static __wrap(ptr) {
4011
+ const obj = Object.create(_MlsGroup.prototype);
4012
+ obj.__wbg_ptr = ptr;
4013
+ MlsGroupFinalization.register(obj, obj.__wbg_ptr, obj);
4014
+ return obj;
4015
+ }
4016
+ __destroy_into_raw() {
4017
+ const ptr = this.__wbg_ptr;
4018
+ this.__wbg_ptr = 0;
4019
+ MlsGroupFinalization.unregister(this);
4020
+ return ptr;
4021
+ }
4022
+ free() {
4023
+ const ptr = this.__destroy_into_raw();
4024
+ wasm.__wbg_mlsgroup_free(ptr, 0);
4025
+ }
4026
+ /**
4027
+ * Stage a member add. Returns `{ commit, welcome?, addedLeafIndices }`
4028
+ * (byte fields as arrays-of-numbers). The group is PENDING after this.
4029
+ * @param {Uint8Array} key_package_msg
4030
+ * @returns {any}
4031
+ */
4032
+ addMember(key_package_msg) {
4033
+ const ptr0 = passArray8ToWasm0(key_package_msg, wasm.__wbindgen_malloc);
4034
+ const len0 = WASM_VECTOR_LEN;
4035
+ const ret = wasm.mlsgroup_addMember(this.__wbg_ptr, ptr0, len0);
4036
+ if (ret[2]) {
4037
+ throw takeFromExternrefTable0(ret[1]);
4038
+ }
4039
+ return takeFromExternrefTable0(ret[0]);
4040
+ }
4041
+ /**
4042
+ * Apply the staged commit (advances the local epoch). Call ONLY after the
4043
+ * server accepted the commit.
4044
+ */
4045
+ applyPendingCommit() {
4046
+ const ret = wasm.mlsgroup_applyPendingCommit(this.__wbg_ptr);
4047
+ if (ret[1]) {
4048
+ throw takeFromExternrefTable0(ret[0]);
4049
+ }
4050
+ }
4051
+ /**
4052
+ * Discard the staged commit without advancing (the 409-rebase path).
4053
+ */
4054
+ clearPendingCommit() {
4055
+ wasm.mlsgroup_clearPendingCommit(this.__wbg_ptr);
4056
+ }
4057
+ /**
4058
+ * Stage a SINGLE atomic add+remove commit. `addKeyPackages` is an
4059
+ * `Array<Uint8Array>` of one-use key packages; `removeMemberIds` an
4060
+ * `Array<Uint8Array>` of member identity bytes. Returns the same
4061
+ * `AddResult` shape as `addMember`. PENDING after this.
4062
+ * @param {Uint8Array[]} add_key_packages
4063
+ * @param {Uint8Array[]} remove_member_ids
4064
+ * @returns {any}
4065
+ */
4066
+ commitChanges(add_key_packages, remove_member_ids) {
4067
+ const ptr0 = passArrayJsValueToWasm0(add_key_packages, wasm.__wbindgen_malloc);
4068
+ const len0 = WASM_VECTOR_LEN;
4069
+ const ptr1 = passArrayJsValueToWasm0(remove_member_ids, wasm.__wbindgen_malloc);
4070
+ const len1 = WASM_VECTOR_LEN;
4071
+ const ret = wasm.mlsgroup_commitChanges(this.__wbg_ptr, ptr0, len0, ptr1, len1);
4072
+ if (ret[2]) {
4073
+ throw takeFromExternrefTable0(ret[1]);
4074
+ }
4075
+ return takeFromExternrefTable0(ret[0]);
4076
+ }
4077
+ /**
4078
+ * The current (applied) epoch as a JS BigInt (u64 exceeds safe-integer).
4079
+ * @returns {bigint}
4080
+ */
4081
+ currentEpoch() {
4082
+ const ret = wasm.mlsgroup_currentEpoch(this.__wbg_ptr);
4083
+ return BigInt.asUintN(64, ret);
4084
+ }
4085
+ /**
4086
+ * Inspect an incoming Commit and return the membership delta it WOULD
4087
+ * effect WITHOUT applying it (the reconciliation gate's crypto-truth).
4088
+ * Returns `{ kind, addedIdentities, removedIdentities }`.
4089
+ * @param {Uint8Array} commit_msg
4090
+ * @returns {any}
4091
+ */
4092
+ describeIncomingCommit(commit_msg) {
4093
+ const ptr0 = passArray8ToWasm0(commit_msg, wasm.__wbindgen_malloc);
4094
+ const len0 = WASM_VECTOR_LEN;
4095
+ const ret = wasm.mlsgroup_describeIncomingCommit(this.__wbg_ptr, ptr0, len0);
4096
+ if (ret[2]) {
4097
+ throw takeFromExternrefTable0(ret[1]);
4098
+ }
4099
+ return takeFromExternrefTable0(ret[0]);
4100
+ }
4101
+ /**
4102
+ * Encrypt an application message at the current epoch. Returns the opaque
4103
+ * MLSMessage bytes (PrivateMessage).
4104
+ * @param {Uint8Array} plaintext
4105
+ * @returns {Uint8Array}
4106
+ */
4107
+ encryptApplicationMessage(plaintext) {
4108
+ const ptr0 = passArray8ToWasm0(plaintext, wasm.__wbindgen_malloc);
4109
+ const len0 = WASM_VECTOR_LEN;
4110
+ const ret = wasm.mlsgroup_encryptApplicationMessage(this.__wbg_ptr, ptr0, len0);
4111
+ if (ret[3]) {
4112
+ throw takeFromExternrefTable0(ret[2]);
4113
+ }
4114
+ var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
4115
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
4116
+ return v2;
4117
+ }
4118
+ /**
4119
+ * Derive the group's current-epoch exporter secret (RFC 9420 §8.5) — the SAME
4120
+ * engine call iOS uses (uniffi `exportSecret`), so a cross-platform call/group
4121
+ * derives a byte-identical key. The SP-4 web call sets this as the LiveKit
4122
+ * frame key (server + SFU stay blind); also group-name/media sealing.
4123
+ * @param {Uint8Array} label
4124
+ * @param {Uint8Array} context
4125
+ * @param {bigint} len
4126
+ * @returns {Uint8Array}
4127
+ */
4128
+ exportSecret(label, context, len) {
4129
+ const ptr0 = passArray8ToWasm0(label, wasm.__wbindgen_malloc);
4130
+ const len0 = WASM_VECTOR_LEN;
4131
+ const ptr1 = passArray8ToWasm0(context, wasm.__wbindgen_malloc);
4132
+ const len1 = WASM_VECTOR_LEN;
4133
+ const ret = wasm.mlsgroup_exportSecret(this.__wbg_ptr, ptr0, len0, ptr1, len1, len);
4134
+ if (ret[3]) {
4135
+ throw takeFromExternrefTable0(ret[2]);
4136
+ }
4137
+ var v3 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
4138
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
4139
+ return v3;
4140
+ }
4141
+ /**
4142
+ * The MLS group id (the rfc_group_id routing key / storage key).
4143
+ * @returns {Uint8Array}
4144
+ */
4145
+ groupId() {
4146
+ const ret = wasm.mlsgroup_groupId(this.__wbg_ptr);
4147
+ var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
4148
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
4149
+ return v1;
4150
+ }
4151
+ /**
4152
+ * True while a staged (unapplied) commit exists.
4153
+ * @returns {boolean}
4154
+ */
4155
+ hasPendingCommit() {
4156
+ const ret = wasm.mlsgroup_hasPendingCommit(this.__wbg_ptr);
4157
+ return ret !== 0;
4158
+ }
4159
+ /**
4160
+ * Process an incoming MLSMessage and apply its effect. Returns a
4161
+ * `ReceivedMessage` plain object (serde-tagged enum): e.g.
4162
+ * `{ Application: { sender, data } }`, `{ CommitApplied: { epoch,
4163
+ * removedSelf } }`, `"Proposal"`, or `"Other"`.
4164
+ * @param {Uint8Array} message
4165
+ * @returns {any}
4166
+ */
4167
+ processIncomingMessage(message) {
4168
+ const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
4169
+ const len0 = WASM_VECTOR_LEN;
4170
+ const ret = wasm.mlsgroup_processIncomingMessage(this.__wbg_ptr, ptr0, len0);
4171
+ if (ret[2]) {
4172
+ throw takeFromExternrefTable0(ret[1]);
4173
+ }
4174
+ return takeFromExternrefTable0(ret[0]);
4175
+ }
4176
+ /**
4177
+ * Stage a member remove (by the member's identity bytes). Returns
4178
+ * `{ commit }`. PENDING after this.
4179
+ * @param {Uint8Array} member_id
4180
+ * @returns {any}
4181
+ */
4182
+ removeMember(member_id) {
4183
+ const ptr0 = passArray8ToWasm0(member_id, wasm.__wbindgen_malloc);
4184
+ const len0 = WASM_VECTOR_LEN;
4185
+ const ret = wasm.mlsgroup_removeMember(this.__wbg_ptr, ptr0, len0);
4186
+ if (ret[2]) {
4187
+ throw takeFromExternrefTable0(ret[1]);
4188
+ }
4189
+ return takeFromExternrefTable0(ret[0]);
4190
+ }
4191
+ /**
4192
+ * Persist the current group state through the JS group storage callback.
4193
+ */
4194
+ writeToStorage() {
4195
+ const ret = wasm.mlsgroup_writeToStorage(this.__wbg_ptr);
4196
+ if (ret[1]) {
4197
+ throw takeFromExternrefTable0(ret[0]);
4198
+ }
4199
+ }
4200
+ };
4201
+ if (Symbol.dispose) MlsGroup.prototype[Symbol.dispose] = MlsGroup.prototype.free;
4202
+ function generateClient(id, group_storage, key_package_storage, signature_keypair) {
4203
+ const ptr0 = passArray8ToWasm0(id, wasm.__wbindgen_malloc);
4204
+ const len0 = WASM_VECTOR_LEN;
4205
+ var ptr1 = isLikeNone(signature_keypair) ? 0 : passArray8ToWasm0(signature_keypair, wasm.__wbindgen_malloc);
4206
+ var len1 = WASM_VECTOR_LEN;
4207
+ const ret = wasm.generateClient(ptr0, len0, group_storage, key_package_storage, ptr1, len1);
4208
+ if (ret[2]) {
4209
+ throw takeFromExternrefTable0(ret[1]);
4210
+ }
4211
+ return MlsClient.__wrap(ret[0]);
4212
+ }
4213
+ function setPanicHook() {
4214
+ wasm.setPanicHook();
4215
+ }
4216
+ function __wbg_BigInt_231999d28f899902() {
4217
+ return handleError(function(arg0) {
4218
+ const ret = BigInt(arg0);
4219
+ return ret;
4220
+ }, arguments);
4221
+ }
4222
+ function __wbg_Error_fdd633d4bb5dd76a(arg0, arg1) {
4223
+ const ret = Error(getStringFromWasm0(arg0, arg1));
4224
+ return ret;
4225
+ }
4226
+ function __wbg_String_8564e559799eccda(arg0, arg1) {
4227
+ const ret = String(arg1);
4228
+ const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
4229
+ const len1 = WASM_VECTOR_LEN;
4230
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
4231
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
4232
+ }
4233
+ function __wbg___wbindgen_bigint_get_as_i64_d9e915702856f831(arg0, arg1) {
4234
+ const v = arg1;
4235
+ const ret = typeof v === "bigint" ? v : void 0;
4236
+ getDataViewMemory0().setBigInt64(arg0 + 8 * 1, isLikeNone(ret) ? BigInt(0) : ret, true);
4237
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
4238
+ }
4239
+ function __wbg___wbindgen_debug_string_8a447059637473e2(arg0, arg1) {
4240
+ const ret = debugString(arg1);
4241
+ const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
4242
+ const len1 = WASM_VECTOR_LEN;
4243
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
4244
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
4245
+ }
4246
+ function __wbg___wbindgen_is_function_acc5528be2b923f2(arg0) {
4247
+ const ret = typeof arg0 === "function";
4248
+ return ret;
4249
+ }
4250
+ function __wbg___wbindgen_is_null_6d937fbfb6478470(arg0) {
4251
+ const ret = arg0 === null;
4252
+ return ret;
4253
+ }
4254
+ function __wbg___wbindgen_is_object_0beba4a1980d3eea(arg0) {
4255
+ const val = arg0;
4256
+ const ret = typeof val === "object" && val !== null;
4257
+ return ret;
4258
+ }
4259
+ function __wbg___wbindgen_is_string_1fca8072260dd261(arg0) {
4260
+ const ret = typeof arg0 === "string";
4261
+ return ret;
4262
+ }
4263
+ function __wbg___wbindgen_is_undefined_721f8decd50c87a3(arg0) {
4264
+ const ret = arg0 === void 0;
4265
+ return ret;
4266
+ }
4267
+ function __wbg___wbindgen_jsval_eq_4e8c38722cb8ff51(arg0, arg1) {
4268
+ const ret = arg0 === arg1;
4269
+ return ret;
4270
+ }
4271
+ function __wbg___wbindgen_memory_9751d9a3017e7c25() {
4272
+ const ret = wasm.memory;
4273
+ return ret;
4274
+ }
4275
+ function __wbg___wbindgen_number_get_1cc01dd708740256(arg0, arg1) {
4276
+ const obj = arg1;
4277
+ const ret = typeof obj === "number" ? obj : void 0;
4278
+ getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
4279
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
4280
+ }
4281
+ function __wbg___wbindgen_string_get_71bb4348194e31f0(arg0, arg1) {
4282
+ const obj = arg1;
4283
+ const ret = typeof obj === "string" ? obj : void 0;
4284
+ var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
4285
+ var len1 = WASM_VECTOR_LEN;
4286
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
4287
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
4288
+ }
4289
+ function __wbg___wbindgen_throw_ea4887a5f8f9a9db(arg0, arg1) {
4290
+ throw new Error(getStringFromWasm0(arg0, arg1));
4291
+ }
4292
+ function __wbg_buffer_7447b9cc2267a9e5(arg0) {
4293
+ const ret = arg0.buffer;
4294
+ return ret;
4295
+ }
4296
+ function __wbg_call_67f43c91d09298f2() {
4297
+ return handleError(function(arg0, arg1, arg2) {
4298
+ const ret = arg0.call(arg1, arg2);
4299
+ return ret;
4300
+ }, arguments);
4301
+ }
4302
+ function __wbg_call_b51415974987aa44() {
4303
+ return handleError(function(arg0, arg1) {
4304
+ const ret = arg0.call(arg1);
4305
+ return ret;
4306
+ }, arguments);
4307
+ }
4308
+ function __wbg_crypto_38df2bab126b63dc(arg0) {
4309
+ const ret = arg0.crypto;
4310
+ return ret;
4311
+ }
4312
+ function __wbg_delete_21fafcbc7bb82c85() {
4313
+ return handleError(function(arg0, arg1) {
4314
+ arg0.delete(arg1);
4315
+ }, arguments);
4316
+ }
4317
+ function __wbg_epoch_15c000ffe3004541() {
4318
+ return handleError(function(arg0, arg1, arg2) {
4319
+ const ret = arg0.epoch(arg1, BigInt.asUintN(64, arg2));
4320
+ return ret;
4321
+ }, arguments);
4322
+ }
4323
+ function __wbg_error_a6fa202b58aa1cd3(arg0, arg1) {
4324
+ let deferred0_0;
4325
+ let deferred0_1;
4326
+ try {
4327
+ deferred0_0 = arg0;
4328
+ deferred0_1 = arg1;
4329
+ console.error(getStringFromWasm0(arg0, arg1));
4330
+ } finally {
4331
+ wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
4332
+ }
4333
+ }
4334
+ function __wbg_getRandomValues_c44a50d8cfdaebeb() {
4335
+ return handleError(function(arg0, arg1) {
4336
+ arg0.getRandomValues(arg1);
4337
+ }, arguments);
4338
+ }
4339
+ function __wbg_get_615446055a48103f() {
4340
+ return handleError(function(arg0, arg1) {
4341
+ const ret = arg0.get(arg1);
4342
+ return ret;
4343
+ }, arguments);
4344
+ }
4345
+ function __wbg_globalThis_6d268067835e6709() {
4346
+ return handleError(function() {
4347
+ const ret = globalThis.globalThis;
4348
+ return ret;
4349
+ }, arguments);
4350
+ }
4351
+ function __wbg_global_3fe6c6c8ad6e6fb2() {
4352
+ return handleError(function() {
4353
+ const ret = global.global;
4354
+ return ret;
4355
+ }, arguments);
4356
+ }
4357
+ function __wbg_insert_2d611f2bf9b60fee() {
4358
+ return handleError(function(arg0, arg1, arg2) {
4359
+ arg0.insert(arg1, arg2);
4360
+ }, arguments);
4361
+ }
4362
+ function __wbg_instanceof_Error_38f854307ecab4ce(arg0) {
4363
+ let result;
4364
+ try {
4365
+ result = arg0 instanceof Error;
4366
+ } catch (_) {
4367
+ result = false;
4368
+ }
4369
+ const ret = result;
4370
+ return ret;
4371
+ }
4372
+ function __wbg_length_c552db98817b9523(arg0) {
4373
+ const ret = arg0.length;
4374
+ return ret;
4375
+ }
4376
+ function __wbg_maxEpochId_0b176768b2f352fb() {
4377
+ return handleError(function(arg0, arg1) {
4378
+ const ret = arg0.maxEpochId(arg1);
4379
+ return ret;
4380
+ }, arguments);
4381
+ }
4382
+ function __wbg_message_a05fcf872473ffc5(arg0) {
4383
+ const ret = arg0.message;
4384
+ return ret;
4385
+ }
4386
+ function __wbg_msCrypto_bd5a034af96bcba6(arg0) {
4387
+ const ret = arg0.msCrypto;
4388
+ return ret;
4389
+ }
4390
+ function __wbg_new_227d7c05414eb861() {
4391
+ const ret = new Error();
4392
+ return ret;
4393
+ }
4394
+ function __wbg_new_32de5cbf49ca7dcb(arg0) {
4395
+ const ret = new Uint8Array(arg0);
4396
+ return ret;
4397
+ }
4398
+ function __wbg_new_364c96143b8f3496() {
4399
+ const ret = new Object();
4400
+ return ret;
4401
+ }
4402
+ function __wbg_new_b8b71ca8104fc178(arg0, arg1) {
4403
+ const ret = new Error(getStringFromWasm0(arg0, arg1));
4404
+ return ret;
4405
+ }
4406
+ function __wbg_new_d9762fd75876aafe() {
4407
+ const ret = new Array();
4408
+ return ret;
4409
+ }
4410
+ function __wbg_new_no_args_4010ad257320fa4f(arg0, arg1) {
4411
+ const ret = new Function(getStringFromWasm0(arg0, arg1));
4412
+ return ret;
4413
+ }
4414
+ function __wbg_new_with_byte_offset_and_length_8b21e3b1308deb48(arg0, arg1, arg2) {
4415
+ const ret = new Uint8Array(arg0, arg1 >>> 0, arg2 >>> 0);
4416
+ return ret;
4417
+ }
4418
+ function __wbg_new_with_length_5fdafe029be917a5(arg0) {
4419
+ const ret = new Uint8Array(arg0 >>> 0);
4420
+ return ret;
4421
+ }
4422
+ function __wbg_node_84ea875411254db1(arg0) {
4423
+ const ret = arg0.node;
4424
+ return ret;
4425
+ }
4426
+ function __wbg_process_44c7a14e11e9f69e(arg0) {
4427
+ const ret = arg0.process;
4428
+ return ret;
4429
+ }
4430
+ function __wbg_push_1303ce035391aed3(arg0, arg1) {
4431
+ const ret = arg0.push(arg1);
4432
+ return ret;
4433
+ }
4434
+ function __wbg_randomFillSync_6c25eac9869eb53c() {
4435
+ return handleError(function(arg0, arg1) {
4436
+ arg0.randomFillSync(arg1);
4437
+ }, arguments);
4438
+ }
4439
+ function __wbg_require_b4edbdcf3e2a1ef0() {
4440
+ return handleError(function() {
4441
+ const ret = module.require;
4442
+ return ret;
4443
+ }, arguments);
4444
+ }
4445
+ function __wbg_self_1035a7cbd1b0d959() {
4446
+ return handleError(function() {
4447
+ const ret = self.self;
4448
+ return ret;
4449
+ }, arguments);
4450
+ }
4451
+ function __wbg_set_047d1ea37bb67c19(arg0, arg1, arg2) {
4452
+ arg0.set(arg1, arg2 >>> 0);
4453
+ }
4454
+ function __wbg_set_1ddc4b8cd44d0da4() {
4455
+ return handleError(function(arg0, arg1, arg2) {
4456
+ const ret = Reflect.set(arg0, arg1, arg2);
4457
+ return ret;
4458
+ }, arguments);
4459
+ }
4460
+ function __wbg_set_28cba565792ec75f(arg0, arg1, arg2) {
4461
+ arg0[arg1 >>> 0] = arg2;
4462
+ }
4463
+ function __wbg_set_6be42768c690e380(arg0, arg1, arg2) {
4464
+ arg0[arg1] = arg2;
4465
+ }
4466
+ function __wbg_set_name_b4c29a3a72ebbddc(arg0, arg1, arg2) {
4467
+ arg0.name = getStringFromWasm0(arg1, arg2);
4468
+ }
4469
+ function __wbg_stack_3b0d974bbf31e44f(arg0, arg1) {
4470
+ const ret = arg1.stack;
4471
+ const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
4472
+ const len1 = WASM_VECTOR_LEN;
4473
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
4474
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
4475
+ }
4476
+ function __wbg_state_bd20456c0f3efbc9() {
4477
+ return handleError(function(arg0, arg1) {
4478
+ const ret = arg0.state(arg1);
4479
+ return ret;
4480
+ }, arguments);
4481
+ }
4482
+ function __wbg_subarray_e0162dcdea48eb3a(arg0, arg1, arg2) {
4483
+ const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0);
4484
+ return ret;
4485
+ }
4486
+ function __wbg_versions_276b2795b1c6a219(arg0) {
4487
+ const ret = arg0.versions;
4488
+ return ret;
4489
+ }
4490
+ function __wbg_window_9c17850b5e99c0ab() {
4491
+ return handleError(function() {
4492
+ const ret = window.window;
4493
+ return ret;
4494
+ }, arguments);
4495
+ }
4496
+ function __wbg_write_56b1cf5bb0e780d6() {
4497
+ return handleError(function(arg0, arg1, arg2, arg3, arg4) {
4498
+ arg0.write(arg1, arg2, arg3, arg4);
4499
+ }, arguments);
4500
+ }
4501
+ function __wbindgen_cast_0000000000000001(arg0) {
4502
+ const ret = arg0;
4503
+ return ret;
4504
+ }
4505
+ function __wbindgen_cast_0000000000000002(arg0, arg1) {
4506
+ const ret = getStringFromWasm0(arg0, arg1);
4507
+ return ret;
4508
+ }
4509
+ function __wbindgen_cast_0000000000000003(arg0) {
4510
+ const ret = BigInt.asUintN(64, arg0);
4511
+ return ret;
4512
+ }
4513
+ function __wbindgen_init_externref_table() {
4514
+ const table = wasm.__wbindgen_externrefs;
4515
+ const offset = table.grow(4);
4516
+ table.set(0, void 0);
4517
+ table.set(offset + 0, void 0);
4518
+ table.set(offset + 1, null);
4519
+ table.set(offset + 2, true);
4520
+ table.set(offset + 3, false);
4521
+ }
4522
+ var MlsClientFinalization = typeof FinalizationRegistry === "undefined" ? { register: () => {
4523
+ }, unregister: () => {
4524
+ } } : new FinalizationRegistry((ptr) => wasm.__wbg_mlsclient_free(ptr, 1));
4525
+ var MlsGroupFinalization = typeof FinalizationRegistry === "undefined" ? { register: () => {
4526
+ }, unregister: () => {
4527
+ } } : new FinalizationRegistry((ptr) => wasm.__wbg_mlsgroup_free(ptr, 1));
4528
+ function addToExternrefTable0(obj) {
4529
+ const idx = wasm.__externref_table_alloc();
4530
+ wasm.__wbindgen_externrefs.set(idx, obj);
4531
+ return idx;
4532
+ }
4533
+ function debugString(val) {
4534
+ const type = typeof val;
4535
+ if (type == "number" || type == "boolean" || val == null) {
4536
+ return `${val}`;
4537
+ }
4538
+ if (type == "string") {
4539
+ return `"${val}"`;
4540
+ }
4541
+ if (type == "symbol") {
4542
+ const description = val.description;
4543
+ if (description == null) {
4544
+ return "Symbol";
4545
+ } else {
4546
+ return `Symbol(${description})`;
4547
+ }
4548
+ }
4549
+ if (type == "function") {
4550
+ const name = val.name;
4551
+ if (typeof name == "string" && name.length > 0) {
4552
+ return `Function(${name})`;
4553
+ } else {
4554
+ return "Function";
4555
+ }
4556
+ }
4557
+ if (Array.isArray(val)) {
4558
+ const length = val.length;
4559
+ let debug = "[";
4560
+ if (length > 0) {
4561
+ debug += debugString(val[0]);
4562
+ }
4563
+ for (let i = 1; i < length; i++) {
4564
+ debug += ", " + debugString(val[i]);
4565
+ }
4566
+ debug += "]";
4567
+ return debug;
4568
+ }
4569
+ const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
4570
+ let className;
4571
+ if (builtInMatches && builtInMatches.length > 1) {
4572
+ className = builtInMatches[1];
4573
+ } else {
4574
+ return toString.call(val);
4575
+ }
4576
+ if (className == "Object") {
4577
+ try {
4578
+ return "Object(" + JSON.stringify(val) + ")";
4579
+ } catch (_) {
4580
+ return "Object";
4581
+ }
4582
+ }
4583
+ if (val instanceof Error) {
4584
+ return `${val.name}: ${val.message}
4585
+ ${val.stack}`;
4586
+ }
4587
+ return className;
4588
+ }
4589
+ function getArrayU8FromWasm0(ptr, len) {
4590
+ ptr = ptr >>> 0;
4591
+ return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
4592
+ }
4593
+ var cachedDataViewMemory0 = null;
4594
+ function getDataViewMemory0() {
4595
+ if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || cachedDataViewMemory0.buffer.detached === void 0 && cachedDataViewMemory0.buffer !== wasm.memory.buffer) {
4596
+ cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
4597
+ }
4598
+ return cachedDataViewMemory0;
4599
+ }
4600
+ function getStringFromWasm0(ptr, len) {
4601
+ return decodeText(ptr >>> 0, len);
4602
+ }
4603
+ var cachedUint8ArrayMemory0 = null;
4604
+ function getUint8ArrayMemory0() {
4605
+ if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
4606
+ cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
4607
+ }
4608
+ return cachedUint8ArrayMemory0;
4609
+ }
4610
+ function handleError(f, args) {
4611
+ try {
4612
+ return f.apply(this, args);
4613
+ } catch (e) {
4614
+ const idx = addToExternrefTable0(e);
4615
+ wasm.__wbindgen_exn_store(idx);
4616
+ }
4617
+ }
4618
+ function isLikeNone(x) {
4619
+ return x === void 0 || x === null;
4620
+ }
4621
+ function passArray8ToWasm0(arg, malloc) {
4622
+ const ptr = malloc(arg.length * 1, 1) >>> 0;
4623
+ getUint8ArrayMemory0().set(arg, ptr / 1);
4624
+ WASM_VECTOR_LEN = arg.length;
4625
+ return ptr;
4626
+ }
4627
+ function passArrayJsValueToWasm0(array, malloc) {
4628
+ const ptr = malloc(array.length * 4, 4) >>> 0;
4629
+ for (let i = 0; i < array.length; i++) {
4630
+ const add = addToExternrefTable0(array[i]);
4631
+ getDataViewMemory0().setUint32(ptr + 4 * i, add, true);
4632
+ }
4633
+ WASM_VECTOR_LEN = array.length;
4634
+ return ptr;
4635
+ }
4636
+ function passStringToWasm0(arg, malloc, realloc) {
4637
+ if (realloc === void 0) {
4638
+ const buf = cachedTextEncoder.encode(arg);
4639
+ const ptr2 = malloc(buf.length, 1) >>> 0;
4640
+ getUint8ArrayMemory0().subarray(ptr2, ptr2 + buf.length).set(buf);
4641
+ WASM_VECTOR_LEN = buf.length;
4642
+ return ptr2;
4643
+ }
4644
+ let len = arg.length;
4645
+ let ptr = malloc(len, 1) >>> 0;
4646
+ const mem = getUint8ArrayMemory0();
4647
+ let offset = 0;
4648
+ for (; offset < len; offset++) {
4649
+ const code = arg.charCodeAt(offset);
4650
+ if (code > 127) break;
4651
+ mem[ptr + offset] = code;
4652
+ }
4653
+ if (offset !== len) {
4654
+ if (offset !== 0) {
4655
+ arg = arg.slice(offset);
4656
+ }
4657
+ ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
4658
+ const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
4659
+ const ret = cachedTextEncoder.encodeInto(arg, view);
4660
+ offset += ret.written;
4661
+ ptr = realloc(ptr, len, offset, 1) >>> 0;
4662
+ }
4663
+ WASM_VECTOR_LEN = offset;
4664
+ return ptr;
4665
+ }
4666
+ function takeFromExternrefTable0(idx) {
4667
+ const value = wasm.__wbindgen_externrefs.get(idx);
4668
+ wasm.__externref_table_dealloc(idx);
4669
+ return value;
4670
+ }
4671
+ var cachedTextDecoder = new TextDecoder("utf-8", { ignoreBOM: true, fatal: true });
4672
+ cachedTextDecoder.decode();
4673
+ var MAX_SAFARI_DECODE_BYTES = 2146435072;
4674
+ var numBytesDecoded = 0;
4675
+ function decodeText(ptr, len) {
4676
+ numBytesDecoded += len;
4677
+ if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
4678
+ cachedTextDecoder = new TextDecoder("utf-8", { ignoreBOM: true, fatal: true });
4679
+ cachedTextDecoder.decode();
4680
+ numBytesDecoded = len;
4681
+ }
4682
+ return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
4683
+ }
4684
+ var cachedTextEncoder = new TextEncoder();
4685
+ if (!("encodeInto" in cachedTextEncoder)) {
4686
+ cachedTextEncoder.encodeInto = function(arg, view) {
4687
+ const buf = cachedTextEncoder.encode(arg);
4688
+ view.set(buf);
4689
+ return {
4690
+ read: arg.length,
4691
+ written: buf.length
4692
+ };
4693
+ };
4694
+ }
4695
+ var WASM_VECTOR_LEN = 0;
4696
+ var wasm;
4697
+ function __wbg_set_wasm(val) {
4698
+ wasm = val;
4699
+ }
4700
+
4701
+ // src/messaging/wasm/pkg/snippets/mls-rs-core-f99cdecbb456b09c/inline0.js
4702
+ var inline0_exports = {};
4703
+ __export(inline0_exports, {
4704
+ date_now: () => date_now
4705
+ });
4706
+ function date_now() {
4707
+ return Date.now();
4708
+ }
4709
+
4710
+ // src/messaging/wasm/loader.ts
4711
+ var import_meta = {};
4712
+ var GLUE_MODULE = "./palbe_mls_bg.js";
4713
+ var SNIPPET_MODULE = "./snippets/mls-rs-core-f99cdecbb456b09c/inline0.js";
4714
+ var inflight = null;
4715
+ var ready = null;
4716
+ async function loadWasmModule() {
4717
+ const wasmUrl = new URL("./pkg/palbe_mls_bg.wasm", import_meta.url);
4718
+ if (wasmUrl.protocol === "file:") {
4719
+ const fsSpecifier = ["node", "fs/promises"].join(":");
4720
+ const { readFile } = await import(
4721
+ /* webpackIgnore: true */
4722
+ fsSpecifier
4723
+ );
4724
+ const bytes = await readFile(wasmUrl);
4725
+ return WebAssembly.compile(bytes);
4726
+ }
4727
+ const response = await fetch(wasmUrl);
4728
+ if (typeof WebAssembly.compileStreaming === "function") {
4729
+ try {
4730
+ return await WebAssembly.compileStreaming(Promise.resolve(response));
4731
+ } catch {
4732
+ }
4733
+ }
4734
+ const buffer = await response.arrayBuffer();
4735
+ return WebAssembly.compile(buffer);
4736
+ }
4737
+ function buildImports() {
4738
+ return {
4739
+ [GLUE_MODULE]: palbe_mls_bg_exports,
4740
+ [SNIPPET_MODULE]: inline0_exports
4741
+ };
4742
+ }
4743
+ function initMls() {
4744
+ if (ready) return Promise.resolve(ready);
4745
+ if (inflight) return inflight;
4746
+ inflight = (async () => {
4747
+ const module2 = await loadWasmModule();
4748
+ const instance = await WebAssembly.instantiate(module2, buildImports());
4749
+ const setWasm = __wbg_set_wasm;
4750
+ setWasm(instance.exports);
4751
+ instance.exports.__wbindgen_start?.();
4752
+ const g = {
4753
+ generateClient,
4754
+ setPanicHook,
4755
+ MlsClient,
4756
+ MlsGroup
4757
+ };
4758
+ try {
4759
+ g.setPanicHook();
4760
+ } catch {
4761
+ }
4762
+ ready = g;
4763
+ return g;
4764
+ })();
4765
+ return inflight;
4766
+ }
4767
+ function requireMls() {
4768
+ if (!ready) {
4769
+ throw new Error("palbe-mls WASM not initialized \u2014 await initMls() first");
4770
+ }
4771
+ return ready;
4772
+ }
4773
+
4774
+ // src/messaging/wasm/bridge.ts
4775
+ function toBytes(v) {
4776
+ if (v instanceof Uint8Array) return v;
4777
+ if (Array.isArray(v)) return Uint8Array.from(v);
4778
+ if (v == null) return new Uint8Array(0);
4779
+ if (ArrayBuffer.isView(v)) return new Uint8Array(v.buffer);
4780
+ throw new Error("expected byte field (Uint8Array | number[])");
4781
+ }
4782
+ function optBytes(v) {
4783
+ if (v == null) return void 0;
4784
+ return toBytes(v);
4785
+ }
4786
+ var MlsGroupHandle = class {
4787
+ /** @internal */
4788
+ constructor(raw) {
4789
+ this.raw = raw;
4790
+ }
4791
+ raw;
4792
+ /** The MLS rfc_group_id (routing key / storage key) bytes. */
4793
+ groupId() {
4794
+ return this.raw.groupId();
4795
+ }
4796
+ /** Current applied epoch (BigInt → number; epochs stay well under 2^53). */
4797
+ currentEpoch() {
4798
+ return Number(this.raw.currentEpoch());
4799
+ }
4800
+ /** Stage a member add (PENDING after). */
4801
+ addMember(keyPackageMsg) {
4802
+ return normalizeAddResult(this.raw.addMember(keyPackageMsg));
4803
+ }
4804
+ /** Stage a member remove by identity bytes (PENDING after). */
4805
+ removeMember(memberId) {
4806
+ const r = this.raw.removeMember(memberId);
4807
+ return { commit: toBytes(r.commit) };
4808
+ }
4809
+ /** Stage a SINGLE atomic add+remove commit (PENDING after). */
4810
+ commitChanges(addKeyPackages, removeMemberIds) {
4811
+ return normalizeAddResult(this.raw.commitChanges(addKeyPackages, removeMemberIds));
4812
+ }
4813
+ /** Apply the staged commit (advance the local epoch). Only after server-accept. */
4814
+ applyPendingCommit() {
4815
+ this.raw.applyPendingCommit();
4816
+ }
4817
+ /** Discard the staged commit without advancing (the 409-rebase path). */
4818
+ clearPendingCommit() {
4819
+ this.raw.clearPendingCommit();
4820
+ }
4821
+ /** True while a staged (unapplied) commit exists. */
4822
+ hasPendingCommit() {
4823
+ return this.raw.hasPendingCommit();
4824
+ }
4825
+ /** Encrypt an application message at the current epoch → PrivateMessage bytes. */
4826
+ encryptApplicationMessage(plaintext) {
4827
+ return this.raw.encryptApplicationMessage(plaintext);
4828
+ }
4829
+ /** Process an incoming MLSMessage and apply its effect (normalized result). */
4830
+ processIncomingMessage(message) {
4831
+ return normalizeReceived(this.raw.processIncomingMessage(message));
4832
+ }
4833
+ /** Inspect an incoming Commit WITHOUT applying it (the gate's crypto-truth). */
4834
+ describeIncomingCommit(commitMsg) {
4835
+ const d = this.raw.describeIncomingCommit(commitMsg);
4836
+ return {
4837
+ kind: d.kind,
4838
+ addedIdentities: d.addedIdentities.map(toBytes),
4839
+ removedIdentities: d.removedIdentities.map(toBytes)
4840
+ };
4841
+ }
4842
+ /** Persist the current group state through the JS group-storage callback. */
4843
+ writeToStorage() {
4844
+ this.raw.writeToStorage();
4845
+ }
4846
+ };
4847
+ function normalizeAddResult(v) {
4848
+ const r = v;
4849
+ return {
4850
+ commit: toBytes(r.commit),
4851
+ welcome: optBytes(r.welcome),
4852
+ addedLeafIndices: (r.addedLeafIndices ?? []).map((n) => Number(n))
4853
+ };
4854
+ }
4855
+ function normalizeReceived(v) {
4856
+ if (v === "Proposal") return { type: "proposal" };
4857
+ if (v === "Other") return { type: "other" };
4858
+ const obj = v;
4859
+ if (obj.Application) {
4860
+ const a = obj.Application;
4861
+ return { type: "application", sender: toBytes(a.sender), data: toBytes(a.data) };
4862
+ }
4863
+ if (obj.CommitApplied) {
4864
+ const c = obj.CommitApplied;
4865
+ return { type: "commitApplied", epoch: Number(c.epoch), removedSelf: Boolean(c.removedSelf) };
4866
+ }
4867
+ return { type: "other" };
4868
+ }
4869
+ var MlsClientHandle = class {
4870
+ /** @internal */
4871
+ constructor(raw) {
4872
+ this.raw = raw;
4873
+ }
4874
+ raw;
4875
+ /** The SP-0 keydir enroll material (`{ signatureKey, credential, capabilities }`). */
4876
+ enrollmentMaterial() {
4877
+ const m = this.raw.enrollmentMaterial();
4878
+ return {
4879
+ signatureKey: toBytes(m.signatureKey),
4880
+ credential: toBytes(m.credential),
4881
+ capabilities: toBytes(m.capabilities)
4882
+ };
4883
+ }
4884
+ /** The device's STABLE signature PUBLIC key bytes (the keydir `signature_key`). */
4885
+ signaturePublicKey() {
4886
+ return this.raw.signaturePublicKey();
4887
+ }
4888
+ /** The MLS Credential wire bytes (the keydir `credential`). */
4889
+ credential() {
4890
+ return this.raw.credential();
4891
+ }
4892
+ /** The MLS Capabilities wire bytes (the keydir `capabilities`). */
4893
+ capabilities() {
4894
+ return this.raw.capabilities();
4895
+ }
4896
+ /** The serialized STABLE signature keypair (secret-bearing — persist sealed). */
4897
+ signatureKeypair() {
4898
+ return this.raw.signatureKeypair();
4899
+ }
4900
+ /** Mint a one-use KeyPackage (RAW RFC 9420 §10 bytes). */
4901
+ generateKeyPackage() {
4902
+ return this.raw.generateKeyPackage();
4903
+ }
4904
+ /** Create a new group at epoch 0 (engine mints the rfc_group_id). */
4905
+ createGroup(groupId) {
4906
+ return new MlsGroupHandle(this.raw.createGroup(groupId ?? void 0));
4907
+ }
4908
+ /** Join a group from a Welcome (opaque MLSMessage bytes). */
4909
+ joinGroup(welcome) {
4910
+ return new MlsGroupHandle(this.raw.joinGroup(welcome));
4911
+ }
4912
+ /** Re-hydrate an already-joined group by its rfc_group_id bytes. */
4913
+ loadGroup(groupId) {
4914
+ return new MlsGroupHandle(this.raw.loadGroup(groupId));
4915
+ }
4916
+ };
4917
+ function generateClient2(id, groupStorage, keyPackageStorage, signatureKeypair) {
4918
+ const { generateClient: gen } = requireMls();
4919
+ return new MlsClientHandle(
4920
+ gen(id, groupStorage, keyPackageStorage, signatureKeypair ?? void 0)
4921
+ );
4922
+ }
4923
+
4924
+ // src/messaging/mls-engine.ts
4925
+ var utf8 = new TextEncoder();
4926
+ var gidHex = bytesToHex;
4927
+ var MlsEngine = class _MlsEngine {
4928
+ constructor(userId, deviceId, client, groupStore, kpStore) {
4929
+ this.userId = userId;
4930
+ this.deviceId = deviceId;
4931
+ this.client = client;
4932
+ this.groupStore = groupStore;
4933
+ this.kpStore = kpStore;
4934
+ }
4935
+ userId;
4936
+ deviceId;
4937
+ client;
4938
+ groupStore;
4939
+ kpStore;
4940
+ groups = /* @__PURE__ */ new Map();
4941
+ chain = Promise.resolve();
4942
+ /**
4943
+ * Build the engine: init the WASM, hydrate the durable stores into the sync
4944
+ * caches, generate (or restore) the device client. Idempotent restore: the
4945
+ * persisted signature keypair keeps the SAME MLS signing identity across
4946
+ * reloads; the group-state cache makes already-joined groups reloadable.
4947
+ */
4948
+ static async create(opts) {
4949
+ await initMls();
4950
+ await Promise.all([opts.groupStore.hydrate(), opts.kpStore.hydrate()]);
4951
+ const persistedKeypair = await opts.sigStore.load(opts.userId);
4952
+ const idBytes = utf8.encode(opts.deviceId);
4953
+ const client = generateClient2(idBytes, opts.groupStore, opts.kpStore, persistedKeypair);
4954
+ if (!persistedKeypair) {
4955
+ await opts.sigStore.save(opts.userId, client.signatureKeypair());
4956
+ }
4957
+ return new _MlsEngine(opts.userId, opts.deviceId, client, opts.groupStore, opts.kpStore);
4958
+ }
4959
+ /** Serialize an engine op onto the chain so the critical section is exclusive. */
4960
+ enqueue(fn) {
4961
+ const run = this.chain.then(fn, fn);
4962
+ this.chain = run.then(
4963
+ () => void 0,
4964
+ () => void 0
4965
+ );
4966
+ return run;
4967
+ }
4968
+ /** Flush both storage caches durably (the post-mutation durability boundary). */
4969
+ async flush() {
4970
+ await Promise.all([this.groupStore.flush(), this.kpStore.flush()]);
4971
+ }
4972
+ // ── Enroll material / key packages ──
4973
+ enrollmentMaterial() {
4974
+ return this.enqueue(() => this.client.enrollmentMaterial());
4975
+ }
4976
+ signaturePublicKey() {
4977
+ return this.enqueue(() => this.client.signaturePublicKey());
4978
+ }
4979
+ generateKeyPackage() {
4980
+ return this.enqueue(async () => {
4981
+ const kp = this.client.generateKeyPackage();
4982
+ await this.flush();
4983
+ return kp;
4984
+ });
4985
+ }
4986
+ // ── Group create / join / reload ──
4987
+ /** Create a group; returns its rfc_group_id (the routing key). */
4988
+ createGroup() {
4989
+ return this.enqueue(async () => {
4990
+ const group = this.client.createGroup();
4991
+ const gid = group.groupId();
4992
+ this.groups.set(gidHex(gid), group);
4993
+ group.writeToStorage();
4994
+ await this.flush();
4995
+ return gid;
4996
+ });
4997
+ }
4998
+ /** Join from a Welcome; returns the joined group's rfc_group_id. */
4999
+ joinFromWelcome(welcome) {
5000
+ return this.enqueue(async () => {
5001
+ const group = this.client.joinGroup(welcome);
5002
+ const gid = group.groupId();
5003
+ this.groups.set(gidHex(gid), group);
5004
+ group.writeToStorage();
5005
+ await this.flush();
5006
+ return gid;
5007
+ });
5008
+ }
5009
+ /** The current applied epoch for a group (0 if unknown). */
5010
+ epoch(rfcGroupId) {
5011
+ return this.enqueue(() => {
5012
+ const g = this.handle(rfcGroupId);
5013
+ return g ? g.currentEpoch() : 0;
5014
+ });
5015
+ }
5016
+ // ── Membership commits (staged → applied/cleared by the rebase loop) ──
5017
+ /** Stage a SINGLE atomic add+remove commit. Returns the AddResult (PENDING). */
5018
+ commitChanges(rfcGroupId, adds, removeMemberIds) {
5019
+ return this.enqueue(() => {
5020
+ const g = this.requireHandle(rfcGroupId);
5021
+ return g.commitChanges(adds, removeMemberIds);
5022
+ });
5023
+ }
5024
+ /** Apply the staged commit (advance the epoch). Only after a server-accept. */
5025
+ applyPendingCommit(rfcGroupId) {
5026
+ return this.enqueue(async () => {
5027
+ const g = this.requireHandle(rfcGroupId);
5028
+ g.applyPendingCommit();
5029
+ g.writeToStorage();
5030
+ await this.flush();
5031
+ });
5032
+ }
5033
+ /** Discard the staged commit (the 409-rebase path). */
5034
+ clearPendingCommit(rfcGroupId) {
5035
+ return this.enqueue(() => {
5036
+ const g = this.handle(rfcGroupId);
5037
+ if (g?.hasPendingCommit()) g.clearPendingCommit();
5038
+ });
5039
+ }
5040
+ // ── Application messages ──
5041
+ /** Encrypt a plaintext at the current epoch → PrivateMessage bytes. */
5042
+ encryptApplication(rfcGroupId, plaintext) {
5043
+ return this.enqueue(async () => {
5044
+ const g = this.requireHandle(rfcGroupId);
5045
+ const ct = g.encryptApplicationMessage(plaintext);
5046
+ g.writeToStorage();
5047
+ await this.flush();
5048
+ return ct;
5049
+ });
5050
+ }
5051
+ /**
5052
+ * Process an incoming MLSMessage (decrypt application / apply commit). Returns
5053
+ * the normalized ReceivedMessage. `describeIncomingCommit` could gate a commit
5054
+ * before applying when the server fans a declaration; today the live server
5055
+ * does not, so we process directly (parity with the iOS nil-declaration path).
5056
+ */
5057
+ processIncoming(rfcGroupId, blob) {
5058
+ return this.enqueue(async () => {
5059
+ const g = this.requireHandle(rfcGroupId);
5060
+ const received = g.processIncomingMessage(blob);
5061
+ g.writeToStorage();
5062
+ await this.flush();
5063
+ return received;
5064
+ });
5065
+ }
5066
+ /** Inspect a commit WITHOUT applying (the reconciliation gate's crypto-truth). */
5067
+ describeIncomingCommit(rfcGroupId, commit) {
5068
+ return this.enqueue(() => this.requireHandle(rfcGroupId).describeIncomingCommit(commit));
5069
+ }
5070
+ /** Persist the current group state (explicit checkpoint). */
5071
+ writeToStorage(rfcGroupId) {
5072
+ return this.enqueue(async () => {
5073
+ const g = this.handle(rfcGroupId);
5074
+ if (g) {
5075
+ g.writeToStorage();
5076
+ await this.flush();
5077
+ }
5078
+ });
5079
+ }
5080
+ // ── Handle resolution ──
5081
+ /** Resolve (or reload from storage) the handle for a group, or undefined. */
5082
+ handle(rfcGroupId) {
5083
+ const key = gidHex(rfcGroupId);
5084
+ const cached = this.groups.get(key);
5085
+ if (cached) return cached;
5086
+ try {
5087
+ const g = this.client.loadGroup(rfcGroupId);
5088
+ this.groups.set(key, g);
5089
+ return g;
5090
+ } catch {
5091
+ return void 0;
5092
+ }
5093
+ }
5094
+ requireHandle(rfcGroupId) {
5095
+ const g = this.handle(rfcGroupId);
5096
+ if (!g) throw new Error(`MLS group not loaded: ${gidHex(rfcGroupId)}`);
5097
+ return g;
5098
+ }
5099
+ };
5100
+
5101
+ // src/messaging/registry.ts
5102
+ var GroupRegistry = class {
5103
+ byRfc = /* @__PURE__ */ new Map();
5104
+ catalog = null;
5105
+ chatFactory = null;
5106
+ onListChange = null;
5107
+ /** Wire the observable chat-list sink + the pointer-stable Chat factory, then
5108
+ * replay the current set so a sink attached after groups loaded reflects them. */
5109
+ attachChatList(onListChange, factory) {
5110
+ this.onListChange = onListChange;
5111
+ this.chatFactory = factory;
5112
+ this.publish();
5113
+ }
5114
+ /** Hydrate from the durable catalog (live entries win — fresher epoch). */
5115
+ async attachCatalog(catalog) {
5116
+ this.catalog = catalog;
5117
+ let changed = false;
5118
+ for (const c of await catalog.all()) {
5119
+ if (!this.byRfc.has(c.rfcGroupId)) {
5120
+ this.byRfc.set(c.rfcGroupId, {
5121
+ displayId: c.displayId,
5122
+ rfcGroupId: c.rfcGroupId,
5123
+ currentEpoch: c.currentEpoch,
5124
+ ownerUserId: c.ownerUserId,
5125
+ directKey: c.directKey,
5126
+ name: c.name
5127
+ });
5128
+ changed = true;
5129
+ }
5130
+ }
5131
+ if (changed) this.publish();
5132
+ }
5133
+ register(group) {
5134
+ const isNew = !this.byRfc.has(group.rfcGroupId);
5135
+ this.byRfc.set(group.rfcGroupId, group);
5136
+ void this.persist(group);
5137
+ if (isNew) this.publish();
5138
+ }
5139
+ remove(rfcGroupId) {
5140
+ if (!this.byRfc.has(rfcGroupId)) return;
5141
+ this.byRfc.delete(rfcGroupId);
5142
+ if (this.catalog) void this.catalog.remove(rfcGroupId);
5143
+ this.publish();
5144
+ }
5145
+ group(rfcGroupId) {
5146
+ return this.byRfc.get(rfcGroupId);
5147
+ }
5148
+ /** The single known group, if exactly one (the wire-gap routing fallback). */
5149
+ soleGroup() {
5150
+ return this.byRfc.size === 1 ? this.byRfc.values().next().value : void 0;
5151
+ }
5152
+ all() {
5153
+ return [...this.byRfc.values()];
5154
+ }
5155
+ /** Advance a cached epoch (after a processed commit). Does NOT publish (the
5156
+ * group SET is unchanged — only metadata). */
5157
+ bumpEpoch(rfcGroupId, epoch) {
5158
+ const g = this.byRfc.get(rfcGroupId);
5159
+ if (!g) return;
5160
+ const bumped = { ...g, currentEpoch: epoch };
5161
+ this.byRfc.set(rfcGroupId, bumped);
5162
+ void this.persist(bumped);
5163
+ }
5164
+ publish() {
5165
+ const factory = this.chatFactory;
5166
+ if (!this.onListChange || !factory) return;
5167
+ const chats = this.all().map((g) => factory(g));
5168
+ this.onListChange(chats);
5169
+ }
5170
+ async persist(group) {
5171
+ if (!this.catalog) return;
5172
+ try {
5173
+ await this.catalog.upsert({
5174
+ displayId: group.displayId,
5175
+ rfcGroupId: group.rfcGroupId,
5176
+ currentEpoch: group.currentEpoch,
5177
+ ownerUserId: group.ownerUserId,
5178
+ directKey: group.directKey,
5179
+ name: group.name
5180
+ });
5181
+ } catch {
5182
+ }
5183
+ }
5184
+ };
5185
+
5186
+ // src/messaging/storage.ts
5187
+ var hasIndexedDB = () => typeof indexedDB !== "undefined" && typeof crypto !== "undefined" && !!crypto.subtle;
5188
+ function copyToArrayBuffer(view) {
5189
+ const out = new ArrayBuffer(view.byteLength);
5190
+ new Uint8Array(out).set(view);
5191
+ return out;
5192
+ }
5193
+ var MemoryKV = class {
5194
+ map = /* @__PURE__ */ new Map();
5195
+ get(key) {
5196
+ return Promise.resolve(this.map.get(key));
5197
+ }
5198
+ set(key, value) {
5199
+ this.map.set(key, value.slice());
5200
+ return Promise.resolve();
5201
+ }
5202
+ delete(key) {
5203
+ this.map.delete(key);
5204
+ return Promise.resolve();
5205
+ }
5206
+ keys(prefix) {
5207
+ return Promise.resolve([...this.map.keys()].filter((k) => k.startsWith(prefix)));
5208
+ }
5209
+ };
5210
+ var DB_NAME = "palbe.messaging";
5211
+ var STORE = "kv";
5212
+ var KEY_STORE = "keys";
5213
+ var SEAL_KEY_ID = "seal-key-v1";
5214
+ function openDb() {
5215
+ return new Promise((resolve, reject) => {
5216
+ const req = indexedDB.open(DB_NAME, 1);
5217
+ req.onupgradeneeded = () => {
5218
+ const db = req.result;
5219
+ if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE);
5220
+ if (!db.objectStoreNames.contains(KEY_STORE)) db.createObjectStore(KEY_STORE);
5221
+ };
5222
+ req.onsuccess = () => resolve(req.result);
5223
+ req.onerror = () => reject(req.error);
5224
+ });
5225
+ }
5226
+ function idbReq(req) {
5227
+ return new Promise((resolve, reject) => {
5228
+ req.onsuccess = () => resolve(req.result);
5229
+ req.onerror = () => reject(req.error);
5230
+ });
5231
+ }
5232
+ async function getSealKey(db) {
5233
+ const existing = await idbReq(
5234
+ db.transaction(KEY_STORE, "readonly").objectStore(KEY_STORE).get(SEAL_KEY_ID)
5235
+ );
5236
+ if (existing) return existing;
5237
+ const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, [
5238
+ "encrypt",
5239
+ "decrypt"
5240
+ ]);
5241
+ await idbReq(db.transaction(KEY_STORE, "readwrite").objectStore(KEY_STORE).put(key, SEAL_KEY_ID));
5242
+ return key;
5243
+ }
5244
+ var IndexedDbKV = class {
5245
+ dbPromise = null;
5246
+ keyPromise = null;
5247
+ db() {
5248
+ if (!this.dbPromise) this.dbPromise = openDb();
5249
+ return this.dbPromise;
5250
+ }
5251
+ async sealKey() {
5252
+ if (!this.keyPromise) this.keyPromise = this.db().then(getSealKey);
5253
+ return this.keyPromise;
5254
+ }
5255
+ async seal(plain) {
5256
+ const key = await this.sealKey();
5257
+ const iv = crypto.getRandomValues(new Uint8Array(12));
5258
+ const ct = new Uint8Array(
5259
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, copyToArrayBuffer(plain))
5260
+ );
5261
+ const out = new Uint8Array(iv.length + ct.length);
5262
+ out.set(iv, 0);
5263
+ out.set(ct, iv.length);
5264
+ return out;
5265
+ }
5266
+ async open(sealed) {
5267
+ const key = await this.sealKey();
5268
+ const iv = copyToArrayBuffer(sealed.subarray(0, 12));
5269
+ const ct = copyToArrayBuffer(sealed.subarray(12));
5270
+ const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
5271
+ return new Uint8Array(pt);
5272
+ }
5273
+ async get(k) {
5274
+ const db = await this.db();
5275
+ const sealed = await idbReq(db.transaction(STORE, "readonly").objectStore(STORE).get(k));
5276
+ if (!sealed) return void 0;
5277
+ return this.open(sealed);
5278
+ }
5279
+ async set(k, v) {
5280
+ const db = await this.db();
5281
+ const sealed = await this.seal(v);
5282
+ await idbReq(db.transaction(STORE, "readwrite").objectStore(STORE).put(sealed, k));
5283
+ }
5284
+ async delete(k) {
5285
+ const db = await this.db();
5286
+ await idbReq(db.transaction(STORE, "readwrite").objectStore(STORE).delete(k));
5287
+ }
5288
+ async keys(prefix) {
5289
+ const db = await this.db();
5290
+ const all = await idbReq(
5291
+ db.transaction(STORE, "readonly").objectStore(STORE).getAllKeys()
5292
+ );
5293
+ return all.map(String).filter((k) => k.startsWith(prefix));
2203
5294
  }
2204
- const doc = document;
2205
- return {
2206
- isHidden: () => doc.visibilityState === "hidden",
2207
- onVisibilityChange: (cb) => {
2208
- doc.addEventListener("visibilitychange", cb);
2209
- return () => doc.removeEventListener("visibilitychange", cb);
2210
- }
2211
- };
5295
+ };
5296
+ function createDurableKV() {
5297
+ return hasIndexedDB() ? new IndexedDbKV() : new MemoryKV();
2212
5298
  }
2213
-
2214
- // src/flags-facade.ts
2215
- function palbeAuthAdapter(auth) {
2216
- return {
2217
- onAuthStateChange(callback) {
2218
- const unsubscribe = auth.onAuthStateChange(() => {
2219
- const signedIn = auth.isSignedIn;
2220
- callback(signedIn ? "SIGNED_IN" : "SIGNED_OUT", signedIn ? { signedIn } : null);
2221
- });
2222
- return { data: { subscription: { unsubscribe } } };
5299
+ function bytesKey(prefix, id) {
5300
+ return prefix + bytesToHex(id);
5301
+ }
5302
+ var GroupStateStorage = class {
5303
+ constructor(kv) {
5304
+ this.kv = kv;
5305
+ }
5306
+ kv;
5307
+ // In-memory authoritative cache (the engine reads/writes this synchronously).
5308
+ stateCache = /* @__PURE__ */ new Map();
5309
+ epochCache = /* @__PURE__ */ new Map();
5310
+ maxEpochCache = /* @__PURE__ */ new Map();
5311
+ // Keys touched since the last flush (durable write queue).
5312
+ dirty = /* @__PURE__ */ new Set();
5313
+ // ── sync engine seam ──
5314
+ state(groupId) {
5315
+ return this.stateCache.get(bytesKey("", groupId));
5316
+ }
5317
+ epoch(groupId, epochId) {
5318
+ return this.epochCache.get(`${bytesKey("", groupId)}:${epochId}`);
5319
+ }
5320
+ write(groupId, groupState, epochInserts, epochUpdates) {
5321
+ const gid = bytesKey("", groupId);
5322
+ this.stateCache.set(gid, groupState.slice());
5323
+ this.dirty.add(`state:${gid}`);
5324
+ let max = this.maxEpochCache.get(gid) ?? -1n;
5325
+ for (const rec of [...epochInserts, ...epochUpdates]) {
5326
+ this.epochCache.set(`${gid}:${rec.id}`, rec.data.slice());
5327
+ this.dirty.add(`epoch:${gid}:${rec.id}`);
5328
+ if (rec.id > max) max = rec.id;
5329
+ }
5330
+ if (max >= 0n) {
5331
+ this.maxEpochCache.set(gid, max);
5332
+ this.dirty.add(`maxEpoch:${gid}`);
5333
+ }
5334
+ }
5335
+ maxEpochId(groupId) {
5336
+ return this.maxEpochCache.get(bytesKey("", groupId));
5337
+ }
5338
+ // ── async durable flush (called by the engine wrapper after a mutating op) ──
5339
+ async flush() {
5340
+ if (this.dirty.size === 0) return;
5341
+ const pending = [...this.dirty];
5342
+ this.dirty.clear();
5343
+ await Promise.all(
5344
+ pending.map(async (key) => {
5345
+ if (key.startsWith("state:")) {
5346
+ const gid = key.slice("state:".length);
5347
+ const v = this.stateCache.get(gid);
5348
+ if (v) await this.kv.set(`gss:state:${gid}`, v);
5349
+ } else if (key.startsWith("epoch:")) {
5350
+ const rest = key.slice("epoch:".length);
5351
+ const v = this.epochCache.get(rest);
5352
+ if (v) await this.kv.set(`gss:epoch:${rest}`, v);
5353
+ } else if (key.startsWith("maxEpoch:")) {
5354
+ const gid = key.slice("maxEpoch:".length);
5355
+ const max = this.maxEpochCache.get(gid);
5356
+ if (max != null) await this.kv.set(`gss:maxEpoch:${gid}`, encodeBigint(max));
5357
+ }
5358
+ })
5359
+ );
5360
+ }
5361
+ /** Hydrate the in-memory cache from the durable KV (call before generateClient). */
5362
+ async hydrate() {
5363
+ const keys = await this.kv.keys("gss:");
5364
+ for (const k of keys) {
5365
+ const v = await this.kv.get(k);
5366
+ if (!v) continue;
5367
+ if (k.startsWith("gss:state:")) {
5368
+ this.stateCache.set(k.slice("gss:state:".length), v);
5369
+ } else if (k.startsWith("gss:epoch:")) {
5370
+ this.epochCache.set(k.slice("gss:epoch:".length), v);
5371
+ } else if (k.startsWith("gss:maxEpoch:")) {
5372
+ this.maxEpochCache.set(k.slice("gss:maxEpoch:".length), decodeBigint(v));
5373
+ }
2223
5374
  }
2224
- };
5375
+ }
5376
+ async wipe() {
5377
+ this.stateCache.clear();
5378
+ this.epochCache.clear();
5379
+ this.maxEpochCache.clear();
5380
+ this.dirty.clear();
5381
+ for (const k of await this.kv.keys("gss:")) await this.kv.delete(k);
5382
+ }
5383
+ };
5384
+ var KeyPackageStorage = class {
5385
+ constructor(kv) {
5386
+ this.kv = kv;
5387
+ }
5388
+ kv;
5389
+ cache = /* @__PURE__ */ new Map();
5390
+ dirtySet = /* @__PURE__ */ new Set();
5391
+ dirtyDelete = /* @__PURE__ */ new Set();
5392
+ // ── sync engine seam ──
5393
+ delete(id) {
5394
+ const k = bytesKey("", id);
5395
+ this.cache.delete(k);
5396
+ this.dirtyDelete.add(k);
5397
+ this.dirtySet.delete(k);
5398
+ }
5399
+ insert(id, data) {
5400
+ const k = bytesKey("", id);
5401
+ this.cache.set(k, data.slice());
5402
+ this.dirtySet.add(k);
5403
+ this.dirtyDelete.delete(k);
5404
+ }
5405
+ get(id) {
5406
+ return this.cache.get(bytesKey("", id));
5407
+ }
5408
+ // ── async durable flush ──
5409
+ async flush() {
5410
+ if (this.dirtySet.size === 0 && this.dirtyDelete.size === 0) return;
5411
+ const sets = [...this.dirtySet];
5412
+ const dels = [...this.dirtyDelete];
5413
+ this.dirtySet.clear();
5414
+ this.dirtyDelete.clear();
5415
+ await Promise.all([
5416
+ ...sets.map((k) => {
5417
+ const v = this.cache.get(k);
5418
+ return v ? this.kv.set(`kps:${k}`, v) : Promise.resolve();
5419
+ }),
5420
+ ...dels.map((k) => this.kv.delete(`kps:${k}`))
5421
+ ]);
5422
+ }
5423
+ async hydrate() {
5424
+ for (const k of await this.kv.keys("kps:")) {
5425
+ const v = await this.kv.get(k);
5426
+ if (v) this.cache.set(k.slice("kps:".length), v);
5427
+ }
5428
+ }
5429
+ async wipe() {
5430
+ this.cache.clear();
5431
+ this.dirtySet.clear();
5432
+ this.dirtyDelete.clear();
5433
+ for (const k of await this.kv.keys("kps:")) await this.kv.delete(k);
5434
+ }
5435
+ };
5436
+ function encodeBigint(n) {
5437
+ return new TextEncoder().encode(n.toString());
2225
5438
  }
2226
- function serverPoolEnv() {
2227
- return {
2228
- now: () => Date.now(),
2229
- setInterval: () => {
2230
- throw new Error("palbe flags: polling is disabled server-side");
2231
- },
2232
- clearInterval: () => {
2233
- },
2234
- storage: null,
2235
- visibility: null
2236
- };
5439
+ function decodeBigint(b) {
5440
+ return BigInt(new TextDecoder().decode(b));
2237
5441
  }
2238
- function sameFlagValue(a, b) {
2239
- if (Object.is(a, b)) return true;
2240
- if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) {
2241
- return JSON.stringify(a) === JSON.stringify(b);
5442
+ var enc = new TextEncoder();
5443
+ var dec = new TextDecoder();
5444
+ var DeviceIdStore = class {
5445
+ constructor(kv) {
5446
+ this.kv = kv;
2242
5447
  }
2243
- return false;
2244
- }
2245
- var PalbeFlags = class {
2246
- transport;
2247
- pool;
5448
+ kv;
5449
+ async load(userId) {
5450
+ const v = await this.kv.get(`devid:${userId}`);
5451
+ return v ? dec.decode(v) : void 0;
5452
+ }
5453
+ async save(userId, deviceId) {
5454
+ await this.kv.set(`devid:${userId}`, enc.encode(deviceId));
5455
+ }
5456
+ async clear(userId) {
5457
+ await this.kv.delete(`devid:${userId}`);
5458
+ }
5459
+ };
5460
+ var SignatureKeyStore = class {
5461
+ constructor(kv) {
5462
+ this.kv = kv;
5463
+ }
5464
+ kv;
5465
+ async load(userId) {
5466
+ return this.kv.get(`sigkey:${userId}`);
5467
+ }
5468
+ async save(userId, keypair) {
5469
+ await this.kv.set(`sigkey:${userId}`, keypair);
5470
+ }
5471
+ };
5472
+
5473
+ // src/messaging/coordinator.ts
5474
+ var MessagingCoordinator = class {
2248
5475
  constructor(rt) {
2249
- this.transport = new FlagsClient(rt.http);
2250
- const browser = typeof document !== "undefined";
2251
- const ref = endpointRefFromApiKey(rt.config.apiKey);
2252
- const options = {
2253
- auth: palbeAuthAdapter(rt.auth),
2254
- ...ref ? { storageKey: `palbe.flags.${ref}` } : {},
2255
- ...browser ? {} : { pollIntervalMs: 0, env: serverPoolEnv() }
5476
+ this.rt = rt;
5477
+ this.kv = createDurableKV();
5478
+ this.deviceStore = new DeviceIdStore(this.kv);
5479
+ this.sigStore = new SignatureKeyStore(this.kv);
5480
+ this.groupStore = new GroupStateStorage(this.kv);
5481
+ this.kpStore = new KeyPackageStorage(this.kv);
5482
+ this.registry.attachChatList(
5483
+ (chats) => {
5484
+ this.chatList = chats;
5485
+ this.emitList();
5486
+ },
5487
+ (group) => this.chatFor(group)
5488
+ );
5489
+ }
5490
+ rt;
5491
+ kv;
5492
+ deviceStore;
5493
+ sigStore;
5494
+ groupStore;
5495
+ kpStore;
5496
+ registry = new GroupRegistry();
5497
+ resolved = null;
5498
+ resolvePromise = null;
5499
+ // Pointer-stable Chat cache (one per rfc id) + the observable chat list.
5500
+ chatByRfc = /* @__PURE__ */ new Map();
5501
+ chatList = [];
5502
+ listListeners = /* @__PURE__ */ new Set();
5503
+ // ── ChatBackend identity ──
5504
+ get selfUserId() {
5505
+ const u = this.rt.auth.currentUser;
5506
+ if (!u) throw new Error("not_signed_in");
5507
+ return u.id;
5508
+ }
5509
+ // ── Public surface (used by the facade) ──
5510
+ /** Self-enroll + start the delivery source (idempotent). The first chat op
5511
+ * calls this implicitly; pb.messaging.start() is the opt-in warm-at-sign-in. */
5512
+ async ensureEnrolled() {
5513
+ await this.resolve();
5514
+ }
5515
+ /** Open the ONE direct chat with userId. INSTANT + LOCAL (no network); the
5516
+ * group materializes on the first send. Pointer-stable per peer pair. */
5517
+ directChat(peerUserId) {
5518
+ const selfId = this.selfUserId;
5519
+ const dk = directKey(selfId, peerUserId);
5520
+ for (const chat2 of this.chatByRfc.values()) {
5521
+ if (chat2.isDirect && chat2.state === "active") {
5522
+ const g = this.registry.all().find((x) => x.displayId === chat2.id);
5523
+ if (g?.directKey === dk) return chat2;
5524
+ }
5525
+ }
5526
+ const draft = {
5527
+ mode: { kind: "direct", peerUserId },
5528
+ reservedId: `draft:${dk}`
2256
5529
  };
2257
- this.pool = new FlagsPool(this.transport, options);
2258
- if (browser) this.pool.start();
5530
+ const cached = this.chatByRfc.get(draft.reservedId);
5531
+ if (cached) return cached;
5532
+ const chat = new Chat({ draft, kind: "direct", backend: this });
5533
+ this.chatByRfc.set(draft.reservedId, chat);
5534
+ void this.ensureEnrolled().catch(() => {
5535
+ });
5536
+ return chat;
2259
5537
  }
2260
- /** Resolves once the first snapshot (or persisted hydrate) is available. Auto-starts the pool. */
2261
- ready() {
2262
- return this.pool.ready();
5538
+ /** Open a multi-party group chat (LOCAL + LAZY; created on first send). */
5539
+ groupChat(members = []) {
5540
+ const draft = {
5541
+ mode: { kind: "group", members },
5542
+ reservedId: `draft:group:${crypto.randomUUID()}`
5543
+ };
5544
+ const chat = new Chat({ draft, kind: "group", backend: this });
5545
+ this.chatByRfc.set(draft.reservedId, chat);
5546
+ void this.ensureEnrolled().catch(() => {
5547
+ });
5548
+ return chat;
2263
5549
  }
2264
- /** Force an immediate re-snapshot. */
2265
- refresh() {
2266
- return this.pool.refresh();
5550
+ /** Look up an active chat by id (grp_ display id). */
5551
+ chatById(id) {
5552
+ const g = this.registry.all().find((x) => x.displayId === id);
5553
+ return g ? this.chatFor(g) : this.chatByRfc.get(id) ?? null;
2267
5554
  }
2268
- /** Frozen snapshot of all cached values (identity-stable until a change). */
2269
- all() {
2270
- return this.pool.all();
5555
+ /** The observable chat list (active chats only). */
5556
+ get chats() {
5557
+ void this.ensureEnrolled().catch(() => {
5558
+ });
5559
+ return this.chatList;
2271
5560
  }
2272
- /** Raw cached value for `key`, or `undefined` when not in the cache. */
2273
- get(key) {
2274
- return this.pool.all()[key];
5561
+ onChatsChange(cb) {
5562
+ this.listListeners.add(cb);
5563
+ void this.ensureEnrolled().catch(() => {
5564
+ });
5565
+ return () => this.listListeners.delete(cb);
5566
+ }
5567
+ emitList() {
5568
+ for (const cb of this.listListeners) cb();
5569
+ }
5570
+ /** Pointer-stable Chat for a group (one per rfc id). */
5571
+ chatFor(group) {
5572
+ const existing = this.chatByRfc.get(group.rfcGroupId);
5573
+ if (existing) return existing;
5574
+ const chat = new Chat({
5575
+ group,
5576
+ kind: group.directKey ? "direct" : "group",
5577
+ backend: this
5578
+ });
5579
+ this.chatByRfc.set(group.rfcGroupId, chat);
5580
+ return chat;
5581
+ }
5582
+ // ── Resolution (build-once) ──
5583
+ resolve() {
5584
+ if (this.resolved) return Promise.resolve(this.resolved);
5585
+ if (this.resolvePromise) return this.resolvePromise;
5586
+ this.resolvePromise = this.buildResolved();
5587
+ return this.resolvePromise;
5588
+ }
5589
+ async buildResolved() {
5590
+ const userId = this.selfUserId;
5591
+ const deviceId = await this.deviceStore.load(userId) ?? mintDeviceId();
5592
+ const engine = await MlsEngine.create({
5593
+ userId,
5594
+ deviceId,
5595
+ groupStore: this.groupStore,
5596
+ kpStore: this.kpStore,
5597
+ sigStore: this.sigStore
5598
+ });
5599
+ const messageStore = new MessageStore(this.kv);
5600
+ const catalog = new GroupCatalog(this.kv);
5601
+ const hub = new MessageHub();
5602
+ const groups = new GroupMessaging(this.rt, engine, userId, deviceId, messageStore);
5603
+ const source = new MessageDeliverySource(
5604
+ this.rt,
5605
+ engine,
5606
+ hub,
5607
+ this.registry,
5608
+ messageStore,
5609
+ deviceId,
5610
+ userId
5611
+ );
5612
+ await enrollDevice(this.rt, engine, this.deviceStore, {});
5613
+ await this.registry.attachCatalog(catalog);
5614
+ const resolved = {
5615
+ engine,
5616
+ groups,
5617
+ source,
5618
+ hub,
5619
+ messageStore,
5620
+ catalog,
5621
+ registry: this.registry,
5622
+ deviceId,
5623
+ userId
5624
+ };
5625
+ this.resolved = resolved;
5626
+ await source.start();
5627
+ return resolved;
5628
+ }
5629
+ // ── ChatBackend impl ──
5630
+ async materialize(draft) {
5631
+ const r = await this.resolve();
5632
+ let group;
5633
+ if (draft.mode.kind === "direct") {
5634
+ group = await r.groups.getOrCreateDirect(draft.mode.peerUserId);
5635
+ } else {
5636
+ group = await r.groups.createGroup();
5637
+ for (const userId of draft.mode.members) {
5638
+ await r.groups.addMember(group, userId);
5639
+ }
5640
+ }
5641
+ r.registry.register(group);
5642
+ void r.catalog.upsert({
5643
+ displayId: group.displayId,
5644
+ rfcGroupId: group.rfcGroupId,
5645
+ currentEpoch: group.currentEpoch,
5646
+ ownerUserId: group.ownerUserId,
5647
+ directKey: group.directKey,
5648
+ name: group.name
5649
+ });
5650
+ return group;
5651
+ }
5652
+ async sendText(group, text, replyTo) {
5653
+ const r = await this.resolve();
5654
+ return r.groups.sendText(group, text, replyTo);
5655
+ }
5656
+ async history(group, limit, before) {
5657
+ const r = await this.resolve();
5658
+ const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
5659
+ const lookup = /* @__PURE__ */ new Map();
5660
+ for (const s of rows) {
5661
+ const cid = s.clientMsgId ?? "";
5662
+ if (cid && s.text !== null) {
5663
+ const senderUserId = s.direction === "outgoing" ? this.selfUserId : "";
5664
+ lookup.set(cid, { text: s.text, senderUserId });
5665
+ }
5666
+ }
5667
+ return rows.map((s) => this.toChatMessage(group, s, (id) => lookup.get(id) ?? null));
5668
+ }
5669
+ toChatMessage(group, s, lookup) {
5670
+ const clientMsgId = s.clientMsgId ?? "";
5671
+ let replyTo = null;
5672
+ if (s.replyTo && lookup) {
5673
+ const ref = {
5674
+ v: 1,
5675
+ client_msg_id: s.replyTo.clientMsgId,
5676
+ preview: {
5677
+ kind: s.replyTo.previewKind,
5678
+ author_user_id: s.replyTo.previewAuthorUserId ?? "",
5679
+ body: s.replyTo.previewBody ?? void 0,
5680
+ body_truncated: false
5681
+ }
5682
+ };
5683
+ replyTo = resolveReply(ref, lookup);
5684
+ }
5685
+ return {
5686
+ id: `${group.displayId}#${s.serverSeq}`,
5687
+ kind: s.text != null ? "text" : "system",
5688
+ direction: s.direction,
5689
+ senderUserId: s.direction === "outgoing" ? this.selfUserId : null,
5690
+ text: s.text,
5691
+ serverSeq: s.serverSeq,
5692
+ sentAt: new Date(s.at),
5693
+ clientMsgId,
5694
+ replyTo
5695
+ };
2275
5696
  }
2276
- /** `true` only when the cached value is strictly `true`; `fallback` when the key is absent. */
2277
- isEnabled(key, fallback = false) {
2278
- return this.pool.isEnabled(key, fallback);
5697
+ async members(group) {
5698
+ const r = await this.resolve();
5699
+ try {
5700
+ const rows = await r.groups.listMembers(group.displayId);
5701
+ const byUser = /* @__PURE__ */ new Map();
5702
+ for (const row of rows) {
5703
+ if (!byUser.has(row.user_id)) {
5704
+ byUser.set(row.user_id, {
5705
+ id: row.user_id,
5706
+ userId: row.user_id,
5707
+ displayName: null,
5708
+ role: row.role,
5709
+ isSelf: row.user_id === this.selfUserId
5710
+ });
5711
+ }
5712
+ }
5713
+ return [...byUser.values()];
5714
+ } catch {
5715
+ return [];
5716
+ }
2279
5717
  }
2280
- /** Alias of {@link isEnabled} (iOS parity). */
2281
- bool(key, fallback = false) {
2282
- return this.isEnabled(key, fallback);
5718
+ async addMember(group, userId) {
5719
+ const r = await this.resolve();
5720
+ await r.groups.addMember(group, userId);
2283
5721
  }
2284
- /** Cached value when it is a string, else `fallback`. */
2285
- getString(key, fallback) {
2286
- const value = this.pool.all()[key];
2287
- return typeof value === "string" ? value : fallback;
5722
+ async removeMember(group, userId) {
5723
+ const r = await this.resolve();
5724
+ await r.groups.removeMember(group, userId);
2288
5725
  }
2289
- /** Cached value when it is an integer number, else `fallback`. */
2290
- getInt(key, fallback) {
2291
- const value = this.pool.all()[key];
2292
- return typeof value === "number" && Number.isInteger(value) ? value : fallback;
5726
+ async leave(group) {
5727
+ const r = await this.resolve();
5728
+ await r.groups.leaveGroup(group);
5729
+ r.registry.remove(group.rfcGroupId);
2293
5730
  }
2294
- /** Cached value when it is a number (integers included), else `fallback`. */
2295
- getDouble(key, fallback) {
2296
- const value = this.pool.all()[key];
2297
- return typeof value === "number" ? value : fallback;
5731
+ registerActive(group) {
5732
+ this.registry.register(group);
2298
5733
  }
2299
- /**
2300
- * Resolve the multivariate variant for `key` — the variant name, or `null`
2301
- * when the flag has no variant (or the read fails).
2302
- *
2303
- * Wire failures (network errors, 404, etc.) resolve to `null`.
2304
- * Invalid flag names throw `BackendError('validation', { code: 'invalid_flag_name' })`.
2305
- *
2306
- * DEVIATION from iOS sync-cache parity: the platform does not propagate
2307
- * variant metadata into the user-flags snapshot/delta cache, so this is an
2308
- * ASYNC transport read (`GET /v1/flags/{key}/variant`), not a cache lookup.
2309
- */
2310
- async getVariant(key) {
2311
- let res;
2312
- try {
2313
- res = await this.transport.getVariant(key);
2314
- } catch (err) {
2315
- throw new BackendError("validation", {
2316
- code: "invalid_flag_name",
2317
- message: err instanceof Error ? err.message : String(err)
5734
+ setTyping(group, isTyping) {
5735
+ void this.resolve().then((r) => r.source.setTyping(group, isTyping));
5736
+ }
5737
+ async markRead(group, upToServerSeq) {
5738
+ const r = await this.resolve();
5739
+ await r.source.markRead(group, upToServerSeq);
5740
+ }
5741
+ subscribeLive(group, chat) {
5742
+ let offMsg = null;
5743
+ let offConv = null;
5744
+ void this.resolve().then((r) => {
5745
+ offMsg = r.hub.onMessage(group.displayId, (m) => {
5746
+ void chat.ingestLive(m);
2318
5747
  });
5748
+ r.source.observeConversation(group);
5749
+ offConv = r.hub.onConv(group.displayId, (e) => chat.applyConv(e.event, e.payload));
5750
+ });
5751
+ return () => {
5752
+ offMsg?.();
5753
+ offConv?.();
5754
+ };
5755
+ }
5756
+ async userIdForDevice(group, deviceId) {
5757
+ const r = await this.resolve();
5758
+ try {
5759
+ const rows = await r.groups.listMembers(group.displayId);
5760
+ return rows.find((m) => m.device_id === deviceId)?.user_id ?? null;
5761
+ } catch {
5762
+ return null;
2319
5763
  }
2320
- return res.data?.name ?? null;
2321
5764
  }
2322
- /** Subscribe to any change in the cached flag set. */
2323
- onChange(callback) {
2324
- return this.pool.onChange(callback);
5765
+ // ── Trust (chat-scoped) ──
5766
+ /** List a user's devices (used by safety-number computation). */
5767
+ async listUserDevices(userId) {
5768
+ const res = await listDevices(this.rt, userId);
5769
+ return res.devices.map((d) => d.device_id);
5770
+ }
5771
+ };
5772
+
5773
+ // src/messaging/facade.ts
5774
+ var PalbeMessaging = class {
5775
+ coordinator;
5776
+ constructor(rt) {
5777
+ this.coordinator = new MessagingCoordinator(rt);
2325
5778
  }
2326
5779
  /**
2327
- * Observe ONE key: fires only when that key's value actually changes
2328
- * (per {@link sameFlagValue} structural compare for objects), with the
2329
- * new value (`undefined` = deleted). P5 React-hook substrate.
5780
+ * Warm messaging at sign-in (optional). Enrolls this device + starts the live
5781
+ * delivery source so incoming DMs/Welcomes drain before the chat list opens.
5782
+ * Never REQUIRED any chat op self-enrolls. Idempotent.
2330
5783
  */
2331
- subscribeKey(key, callback) {
2332
- let last = this.pool.all()[key];
2333
- return this.pool.onChange(() => {
2334
- const next = this.pool.all()[key];
2335
- if (sameFlagValue(last, next)) return;
2336
- last = next;
2337
- callback(next);
2338
- });
5784
+ async start() {
5785
+ await this.coordinator.ensureEnrolled();
2339
5786
  }
2340
5787
  /**
2341
- * Async iteration over flag changes: yields the new {@link all} view on
2342
- * every pool change notification. The listener is detached when the
2343
- * consumer `break`s/`return`s/`throw`s including while a `next()` is
2344
- * still pending (it resolves `{ done: true }` instead of hanging, which a
2345
- * plain async-generator `finally` would not guarantee).
5788
+ * Open the ONE direct chat with `userId`. INSTANT + LOCAL no network, nothing
5789
+ * created yet. Returns a draft `Chat` to show immediately; the server group +
5790
+ * MLS material materialize LAZILY on the first `chat.send`. Idempotent + stable:
5791
+ * the same two users always resolve to the SAME Chat.
2346
5792
  */
2347
- changes() {
2348
- const queue = [];
2349
- const pending = [];
2350
- let finished = false;
2351
- const unsubscribe = this.pool.onChange(() => {
2352
- const snapshot = this.pool.all();
2353
- const resolve = pending.shift();
2354
- if (resolve) resolve({ value: snapshot, done: false });
2355
- else queue.push(snapshot);
2356
- });
2357
- const finish = () => {
2358
- if (finished) return;
2359
- finished = true;
2360
- unsubscribe();
2361
- while (pending.length > 0) pending.shift()?.({ value: void 0, done: true });
2362
- };
2363
- return {
2364
- next: () => {
2365
- if (finished) return Promise.resolve({ value: void 0, done: true });
2366
- const head = queue.shift();
2367
- if (head !== void 0) return Promise.resolve({ value: head, done: false });
2368
- return new Promise((resolve) => {
2369
- pending.push(resolve);
2370
- });
2371
- },
2372
- return: () => {
2373
- finish();
2374
- return Promise.resolve({ value: void 0, done: true });
2375
- },
2376
- throw: (error) => {
2377
- finish();
2378
- return Promise.reject(error);
2379
- },
2380
- [Symbol.asyncIterator]() {
2381
- return this;
2382
- }
2383
- };
5793
+ directChat(userId) {
5794
+ return this.coordinator.directChat(userId);
2384
5795
  }
2385
- /** Stop polling and detach all listeners (auth + visibility + subscribers). */
2386
- destroy() {
2387
- this.pool.destroy();
5796
+ /**
5797
+ * Open a multi-party group chat. LOCAL + LAZY like `directChat`: pick the
5798
+ * members, get a draft Chat, the group is created on the FIRST `chat.send`.
5799
+ */
5800
+ groupChat(opts = {}) {
5801
+ return this.coordinator.groupChat(opts.members ?? []);
5802
+ }
5803
+ /** Look up a chat by id (e.g. from a push payload / deep link). null if unknown. */
5804
+ chat(id) {
5805
+ return this.coordinator.chatById(id);
5806
+ }
5807
+ /** The observable chat list (DMs + groups, active only). Read it after an
5808
+ * `onChatsChange` subscription to render the inbox. */
5809
+ get chats() {
5810
+ return this.coordinator.chats;
5811
+ }
5812
+ /** Subscribe to chat-list changes (a chat added/removed/hydrated). */
5813
+ onChatsChange(cb) {
5814
+ return this.coordinator.onChatsChange(cb);
2388
5815
  }
2389
5816
  };
2390
5817
 
@@ -2732,10 +6159,10 @@ var RealtimeSocket = class {
2732
6159
  this.flushPending(topic);
2733
6160
  }
2734
6161
  flushPending(topic) {
2735
- const ready = this.pendingBroadcasts.filter((p) => p.topic === topic);
2736
- if (ready.length === 0) return;
6162
+ const ready2 = this.pendingBroadcasts.filter((p) => p.topic === topic);
6163
+ if (ready2.length === 0) return;
2737
6164
  this.pendingBroadcasts = this.pendingBroadcasts.filter((p) => p.topic !== topic);
2738
- for (const p of ready) this.sendBroadcast(p.topic, p.event, p.payload);
6165
+ for (const p of ready2) this.sendBroadcast(p.topic, p.event, p.payload);
2739
6166
  }
2740
6167
  startHeartbeat() {
2741
6168
  this.stopHeartbeat();
@@ -3075,7 +6502,7 @@ function defaultSessionStorage(key) {
3075
6502
  }
3076
6503
 
3077
6504
  // src/version.ts
3078
- var VERSION = "1.0.0";
6505
+ var VERSION = "1.1.0";
3079
6506
 
3080
6507
  // src/runtime.ts
3081
6508
  function buildRuntime(config) {
@@ -3126,6 +6553,8 @@ function buildRuntime(config) {
3126
6553
  let flags;
3127
6554
  let realtime;
3128
6555
  let analytics;
6556
+ let calls;
6557
+ let messaging;
3129
6558
  const rt = {
3130
6559
  config,
3131
6560
  http,
@@ -3149,6 +6578,17 @@ function buildRuntime(config) {
3149
6578
  if (!realtime) realtime = new PalbeRealtime(rt);
3150
6579
  return realtime;
3151
6580
  },
6581
+ // PalbeCalls is lazy + stateless per instance (no timer/socket to destroy).
6582
+ get calls() {
6583
+ if (!calls) calls = new PalbeCalls(rt);
6584
+ return calls;
6585
+ },
6586
+ // Lazy like the others — constructing PalbeMessaging is inert (no enroll /
6587
+ // socket / WASM load until the first chat op or pb.messaging.start()).
6588
+ get messaging() {
6589
+ if (!messaging) messaging = new PalbeMessaging(rt);
6590
+ return messaging;
6591
+ },
3152
6592
  destroyRealtime() {
3153
6593
  realtime?.destroy();
3154
6594
  realtime = void 0;
@@ -3338,6 +6778,12 @@ function createClientProxy(resolveRt, nsAccessor) {
3338
6778
  },
3339
6779
  get analytics() {
3340
6780
  return resolveRt().analytics;
6781
+ },
6782
+ get calls() {
6783
+ return resolveRt().calls;
6784
+ },
6785
+ get messaging() {
6786
+ return resolveRt().messaging;
3341
6787
  }
3342
6788
  };
3343
6789
  return new Proxy(base, {