@palbase/web 1.0.0 → 1.1.0

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