@palbase/web 1.0.1 → 1.1.0

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