@palbase/web 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{analytics-facade-Cq7R-VH_.d.ts → analytics-facade-CbXprYrS.d.ts} +72 -0
- package/dist/{analytics-facade-B8UATgS4.d.cts → analytics-facade-DA9TGKLF.d.cts} +72 -0
- package/dist/{chunk-UX43AB4W.js → chunk-3EVGYJ5F.js} +380 -15
- package/dist/chunk-3EVGYJ5F.js.map +1 -0
- package/dist/index.cjs +379 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/internal.cjs +379 -14
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +3 -3
- package/dist/internal.d.ts +3 -3
- package/dist/internal.js +1 -1
- package/dist/next/client.cjs +379 -14
- package/dist/next/client.cjs.map +1 -1
- package/dist/next/client.js +1 -1
- package/dist/next/index.cjs +379 -14
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +2 -2
- package/dist/next/index.d.ts +2 -2
- package/dist/next/index.js +1 -1
- package/dist/{pb-DSJiH1uV.d.ts → pb-CLfTLQzH.d.ts} +1 -1
- package/dist/{pb-DivIiUGR.d.cts → pb-SVs7vTOp.d.cts} +1 -1
- package/dist/react/index.cjs +1 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-UX43AB4W.js.map +0 -1
package/dist/next/client.js
CHANGED
package/dist/next/index.cjs
CHANGED
|
@@ -2628,6 +2628,69 @@ var PalbeFlags = class {
|
|
|
2628
2628
|
}
|
|
2629
2629
|
};
|
|
2630
2630
|
|
|
2631
|
+
// src/messaging/delete-fold.ts
|
|
2632
|
+
var DeleteFold = class {
|
|
2633
|
+
// targets with a confirmed valid tombstone — MONOTONIC (never removed within retention).
|
|
2634
|
+
tombstoned = /* @__PURE__ */ new Set();
|
|
2635
|
+
// target → the tombstone's authenticated actor userId, awaiting the target's arrival.
|
|
2636
|
+
pending = /* @__PURE__ */ new Map();
|
|
2637
|
+
// dedup of real wire events the fold could evaluate (tombstoned or parked in pending).
|
|
2638
|
+
seen = /* @__PURE__ */ new Set();
|
|
2639
|
+
// events parked because NEITHER the actor NOR the target's author was resolvable at ingest;
|
|
2640
|
+
// keyed by eventClientMsgId so a re-delivered unverifiable event is held exactly once.
|
|
2641
|
+
held = [];
|
|
2642
|
+
/**
|
|
2643
|
+
* Ingest one tombstone. `authorOfTarget` resolves the target message's AUTHOR
|
|
2644
|
+
* userId (null = target absent locally → defer).
|
|
2645
|
+
*/
|
|
2646
|
+
ingest(e, authorOfTarget) {
|
|
2647
|
+
if (this.tombstoned.has(e.targetClientMsgId)) return;
|
|
2648
|
+
if (this.seen.has(e.eventClientMsgId)) return;
|
|
2649
|
+
if (this.heldContains(e.eventClientMsgId)) return;
|
|
2650
|
+
const author = authorOfTarget(e.targetClientMsgId);
|
|
2651
|
+
if (author !== null) {
|
|
2652
|
+
this.seen.add(e.eventClientMsgId);
|
|
2653
|
+
if (e.actorUserId === null || e.actorUserId !== author) return;
|
|
2654
|
+
this.tombstoned.add(e.targetClientMsgId);
|
|
2655
|
+
} else if (e.actorUserId !== null) {
|
|
2656
|
+
this.seen.add(e.eventClientMsgId);
|
|
2657
|
+
this.pending.set(e.targetClientMsgId, e.actorUserId);
|
|
2658
|
+
} else {
|
|
2659
|
+
this.held.push(e);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
/** True once a valid tombstone has absorbed this target. */
|
|
2663
|
+
isTombstoned(targetClientMsgId) {
|
|
2664
|
+
return this.tombstoned.has(targetClientMsgId);
|
|
2665
|
+
}
|
|
2666
|
+
/**
|
|
2667
|
+
* When a target message newly arrives with a resolved `author`, re-check any
|
|
2668
|
+
* pending tombstone for it AND re-attempt any held (unverifiable) tombstones
|
|
2669
|
+
* whose target is now resolvable. The deferred gate is the SAME comparison as
|
|
2670
|
+
* the in-order path.
|
|
2671
|
+
*/
|
|
2672
|
+
reevaluatePending(target, author) {
|
|
2673
|
+
const actor = this.pending.get(target);
|
|
2674
|
+
if (actor !== void 0) {
|
|
2675
|
+
if (author !== null && actor === author) {
|
|
2676
|
+
this.tombstoned.add(target);
|
|
2677
|
+
this.pending.delete(target);
|
|
2678
|
+
} else if (author !== null) {
|
|
2679
|
+
this.pending.delete(target);
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
if (this.held.length === 0) return;
|
|
2683
|
+
const pendingHeld = this.held;
|
|
2684
|
+
this.held = [];
|
|
2685
|
+
for (const e of pendingHeld) {
|
|
2686
|
+
this.ingest(e, (t) => t === target ? author : null);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
heldContains(eventClientMsgId) {
|
|
2690
|
+
return this.held.some((h) => h.eventClientMsgId === eventClientMsgId);
|
|
2691
|
+
}
|
|
2692
|
+
};
|
|
2693
|
+
|
|
2631
2694
|
// src/messaging/edit-fold.ts
|
|
2632
2695
|
function orderLt(aEpoch, aSeq, bEpoch, bSeq) {
|
|
2633
2696
|
if (aEpoch !== bEpoch) return aEpoch < bEpoch;
|
|
@@ -2849,6 +2912,17 @@ async function listDevices(rt, userId) {
|
|
|
2849
2912
|
}
|
|
2850
2913
|
|
|
2851
2914
|
// src/messaging/group-messaging.ts
|
|
2915
|
+
function encodeDelete(args) {
|
|
2916
|
+
return encodeUtf8(
|
|
2917
|
+
JSON.stringify({
|
|
2918
|
+
v: 1,
|
|
2919
|
+
type: "delete",
|
|
2920
|
+
client_msg_id: args.clientMsgId,
|
|
2921
|
+
target_client_msg_id: args.targetClientMsgId,
|
|
2922
|
+
scope: "everyone"
|
|
2923
|
+
})
|
|
2924
|
+
);
|
|
2925
|
+
}
|
|
2852
2926
|
function encodeEdit(args) {
|
|
2853
2927
|
return encodeUtf8(
|
|
2854
2928
|
JSON.stringify({
|
|
@@ -2886,6 +2960,18 @@ function decodeEnvelope(bytes) {
|
|
|
2886
2960
|
const s = decodeUtf8(bytes);
|
|
2887
2961
|
try {
|
|
2888
2962
|
const o = JSON.parse(s);
|
|
2963
|
+
if (typeof o === "object" && o !== null && o.type === "delete") {
|
|
2964
|
+
return {
|
|
2965
|
+
type: "delete",
|
|
2966
|
+
text: null,
|
|
2967
|
+
clientMsgId: o.client_msg_id ?? "",
|
|
2968
|
+
replyTo: null,
|
|
2969
|
+
delete: {
|
|
2970
|
+
targetClientMsgId: o.target_client_msg_id ?? "",
|
|
2971
|
+
scope: o.scope ?? "everyone"
|
|
2972
|
+
}
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2889
2975
|
if (typeof o === "object" && o !== null && o.type === "reaction") {
|
|
2890
2976
|
return {
|
|
2891
2977
|
type: "reaction",
|
|
@@ -3273,6 +3359,55 @@ var GroupMessaging = class {
|
|
|
3273
3359
|
clientMsgId: args.clientMsgId
|
|
3274
3360
|
};
|
|
3275
3361
|
}
|
|
3362
|
+
/** Send a delete-for-everyone tombstone on a target message. Encrypts a
|
|
3363
|
+
* `type:'delete'` envelope at the current epoch and sends through the SAME MLS
|
|
3364
|
+
* application path as `sendText` (the server stays blind — a delete is just
|
|
3365
|
+
* another opaque application message; the original ciphertext row is NOT
|
|
3366
|
+
* removed). Persists the outgoing delete row so the tombstone re-folds onto its
|
|
3367
|
+
* target after a reload (the own-send half of the reload parity — the iOS-review
|
|
3368
|
+
* CRITICAL boundary; the projection's `.delete` branch re-folds it). NEVER
|
|
3369
|
+
* rebases (epoch-bound like any application message). */
|
|
3370
|
+
async sendDelete(group, args) {
|
|
3371
|
+
const plaintext = encodeDelete({
|
|
3372
|
+
clientMsgId: args.clientMsgId,
|
|
3373
|
+
targetClientMsgId: args.targetClientMsgId
|
|
3374
|
+
});
|
|
3375
|
+
const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
|
|
3376
|
+
const body = {
|
|
3377
|
+
ciphertext_b64: toBase64(ct),
|
|
3378
|
+
client_idem_key: randomId()
|
|
3379
|
+
};
|
|
3380
|
+
const wire = await palbeRequest(
|
|
3381
|
+
this.rt,
|
|
3382
|
+
"POST",
|
|
3383
|
+
MessagingPaths.groupMessages(group.displayId),
|
|
3384
|
+
{ body }
|
|
3385
|
+
);
|
|
3386
|
+
const stored = {
|
|
3387
|
+
id: `${group.rfcGroupId}#${wire.server_seq}`,
|
|
3388
|
+
direction: "outgoing",
|
|
3389
|
+
text: null,
|
|
3390
|
+
senderDeviceId: this.selfDeviceId,
|
|
3391
|
+
epoch: wire.epoch,
|
|
3392
|
+
serverSeq: wire.server_seq,
|
|
3393
|
+
at: Date.now(),
|
|
3394
|
+
clientMsgId: args.clientMsgId,
|
|
3395
|
+
replyTo: null,
|
|
3396
|
+
envelopeType: "delete",
|
|
3397
|
+
delete: {
|
|
3398
|
+
targetClientMsgId: args.targetClientMsgId,
|
|
3399
|
+
scope: "everyone"
|
|
3400
|
+
}
|
|
3401
|
+
};
|
|
3402
|
+
try {
|
|
3403
|
+
await this.messageStore.append(group.rfcGroupId, stored);
|
|
3404
|
+
} catch {
|
|
3405
|
+
}
|
|
3406
|
+
return {
|
|
3407
|
+
receipt: { serverSeq: wire.server_seq, epoch: wire.epoch },
|
|
3408
|
+
clientMsgId: args.clientMsgId
|
|
3409
|
+
};
|
|
3410
|
+
}
|
|
3276
3411
|
// ── The rebase loop ──
|
|
3277
3412
|
async commitWithRebase(rfcGroupId, build) {
|
|
3278
3413
|
const gidBytes = fromBase64(rfcGroupId);
|
|
@@ -3390,6 +3525,7 @@ var ReactionFold = class {
|
|
|
3390
3525
|
};
|
|
3391
3526
|
|
|
3392
3527
|
// src/messaging/chat.ts
|
|
3528
|
+
var DELETED_DESCRIPTOR = "\u{1F6AB} This message was deleted";
|
|
3393
3529
|
var Chat = class {
|
|
3394
3530
|
/** Stable, URL/log-safe id (the grp_ display id once active; a reserved local id while draft). */
|
|
3395
3531
|
id;
|
|
@@ -3411,6 +3547,15 @@ var Chat = class {
|
|
|
3411
3547
|
reactionFold = new ReactionFold();
|
|
3412
3548
|
/** The single authoritative edit fold for this chat (live + own-send + history). */
|
|
3413
3549
|
editFold = new EditFold();
|
|
3550
|
+
/** The single authoritative delete-for-everyone fold (live + own-send + history).
|
|
3551
|
+
* A tombstone scrubs its target in place (delete DOMINATES edit at render). */
|
|
3552
|
+
deleteFold = new DeleteFold();
|
|
3553
|
+
/** delete-for-me suppression keys (clientMsgId, or `seq:<serverSeq>` for legacy)
|
|
3554
|
+
* — the message is OMITTED from this view. Local + persisted per chat, NO wire. */
|
|
3555
|
+
suppressed = /* @__PURE__ */ new Set();
|
|
3556
|
+
/** True once the persisted suppression set has been loaded (so the omit applies
|
|
3557
|
+
* even on the cold-launch hydrate path before a fresh deleteForMe). */
|
|
3558
|
+
suppressedLoaded = false;
|
|
3414
3559
|
/** Per-target BASE (original) text, so the rendered text is `edit ?? original`.
|
|
3415
3560
|
* Seeded once per target (write-once base) so a 2nd own edit can't corrupt it. */
|
|
3416
3561
|
originalTextByClientMsgId = /* @__PURE__ */ new Map();
|
|
@@ -3459,7 +3604,7 @@ var Chat = class {
|
|
|
3459
3604
|
return this.kind === "direct";
|
|
3460
3605
|
}
|
|
3461
3606
|
get messages() {
|
|
3462
|
-
return this.
|
|
3607
|
+
return this.surfaced();
|
|
3463
3608
|
}
|
|
3464
3609
|
get members() {
|
|
3465
3610
|
return this.memberCache;
|
|
@@ -3468,13 +3613,49 @@ var Chat = class {
|
|
|
3468
3613
|
return this.typingList;
|
|
3469
3614
|
}
|
|
3470
3615
|
get lastMessage() {
|
|
3471
|
-
return this.
|
|
3616
|
+
return this.surfaced().at(-1) ?? null;
|
|
3472
3617
|
}
|
|
3473
3618
|
get unreadCount() {
|
|
3474
|
-
return this.
|
|
3475
|
-
(m) => m.direction === "incoming" && m.serverSeq > this.readWatermark
|
|
3619
|
+
return this.surfaced().filter(
|
|
3620
|
+
(m) => m.direction === "incoming" && !m.isDeleted && m.serverSeq > this.readWatermark
|
|
3476
3621
|
).length;
|
|
3477
3622
|
}
|
|
3623
|
+
/**
|
|
3624
|
+
* The RENDER PRECEDENCE — the single composition point (live AND history project
|
|
3625
|
+
* through it identically). Over the raw `messageList` (which already carries the
|
|
3626
|
+
* folded edit text + reactions + reply):
|
|
3627
|
+
* (1) in the delete-for-me suppression set → OMIT the message entirely;
|
|
3628
|
+
* (2) else tombstoned (delete-for-everyone) → the neutral "deleted" descriptor
|
|
3629
|
+
* with reactions/reply/edit HIDDEN (delete DOMINATES edit — short-circuit);
|
|
3630
|
+
* (3) else the row as-is (edit overlay + reactions + reply already applied).
|
|
3631
|
+
* Pure over (messageList, deleteFold, suppressed) — recomputed on every read so a
|
|
3632
|
+
* just-folded delete / just-suppressed key takes effect without rewriting rows.
|
|
3633
|
+
*/
|
|
3634
|
+
surfaced() {
|
|
3635
|
+
const out = [];
|
|
3636
|
+
for (const m of this.messageList) {
|
|
3637
|
+
const key = this.suppressionKey(m);
|
|
3638
|
+
if (this.suppressed.has(key)) continue;
|
|
3639
|
+
const tombstoned = m.clientMsgId && this.deleteFold.isTombstoned(m.clientMsgId) || m.isDeleted;
|
|
3640
|
+
if (tombstoned) {
|
|
3641
|
+
out.push({
|
|
3642
|
+
...m,
|
|
3643
|
+
text: DELETED_DESCRIPTOR,
|
|
3644
|
+
reactions: {},
|
|
3645
|
+
replyTo: null,
|
|
3646
|
+
edited: false,
|
|
3647
|
+
isDeleted: true
|
|
3648
|
+
});
|
|
3649
|
+
continue;
|
|
3650
|
+
}
|
|
3651
|
+
out.push(m);
|
|
3652
|
+
}
|
|
3653
|
+
return out;
|
|
3654
|
+
}
|
|
3655
|
+
/** The delete-for-me suppression key: clientMsgId when present, else `seq:<n>`. */
|
|
3656
|
+
suppressionKey(m) {
|
|
3657
|
+
return m.clientMsgId ? m.clientMsgId : `seq:${m.serverSeq}`;
|
|
3658
|
+
}
|
|
3478
3659
|
get title() {
|
|
3479
3660
|
if (this.titleOverride) return this.titleOverride;
|
|
3480
3661
|
if (this._group?.name) return this._group.name;
|
|
@@ -3498,9 +3679,28 @@ var Chat = class {
|
|
|
3498
3679
|
if (this.wired || this._state !== "active" || !this._group) return;
|
|
3499
3680
|
this.wired = true;
|
|
3500
3681
|
this.liveUnsub = this.backend.subscribeLive(this._group, this);
|
|
3682
|
+
void this.loadSuppressed();
|
|
3501
3683
|
void this.hydrateHistory();
|
|
3502
3684
|
void this.refreshMembers();
|
|
3503
3685
|
}
|
|
3686
|
+
/** Hydrate the persisted delete-for-me suppression keys (once), then re-emit so
|
|
3687
|
+
* any already-surfaced suppressed message is omitted (cold-launch parity). */
|
|
3688
|
+
async loadSuppressed() {
|
|
3689
|
+
if (this.suppressedLoaded || !this._group) return;
|
|
3690
|
+
this.suppressedLoaded = true;
|
|
3691
|
+
try {
|
|
3692
|
+
const keys = await this.backend.loadSuppressed(this._group);
|
|
3693
|
+
let changed = false;
|
|
3694
|
+
for (const k of keys) {
|
|
3695
|
+
if (!this.suppressed.has(k)) {
|
|
3696
|
+
this.suppressed.add(k);
|
|
3697
|
+
changed = true;
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
if (changed) this.emit();
|
|
3701
|
+
} catch {
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3504
3704
|
async hydrateHistory() {
|
|
3505
3705
|
if (this.historyLoaded || !this._group) return;
|
|
3506
3706
|
this.historyLoaded = true;
|
|
@@ -3511,13 +3711,13 @@ var Chat = class {
|
|
|
3511
3711
|
let changed = false;
|
|
3512
3712
|
for (const m of incoming) {
|
|
3513
3713
|
if (m.serverSeq <= 0) continue;
|
|
3514
|
-
if (m.clientMsgId && m.text !== null) {
|
|
3714
|
+
if (m.clientMsgId && m.text !== null && !m.isDeleted) {
|
|
3515
3715
|
this.byClientMsgId.set(m.clientMsgId, {
|
|
3516
3716
|
text: m.text,
|
|
3517
3717
|
senderUserId: m.senderUserId ?? ""
|
|
3518
3718
|
});
|
|
3519
3719
|
}
|
|
3520
|
-
if (m.clientMsgId) {
|
|
3720
|
+
if (m.clientMsgId && !m.isDeleted) {
|
|
3521
3721
|
this.seedEditBase(m.clientMsgId, m.text, m.senderUserId ?? "");
|
|
3522
3722
|
}
|
|
3523
3723
|
}
|
|
@@ -3527,6 +3727,9 @@ var Chat = class {
|
|
|
3527
3727
|
const key = this.internalKey(m.serverSeq);
|
|
3528
3728
|
if (this.seenKeys.has(key)) continue;
|
|
3529
3729
|
this.seenKeys.add(key);
|
|
3730
|
+
if (m.clientMsgId && !m.isDeleted) {
|
|
3731
|
+
this.deleteFold.reevaluatePending(m.clientMsgId, m.senderUserId);
|
|
3732
|
+
}
|
|
3530
3733
|
this.messageList.push(this.applyEditOverlay(this.applyReactionTally(m)));
|
|
3531
3734
|
changed = true;
|
|
3532
3735
|
this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
|
|
@@ -3584,6 +3787,21 @@ var Chat = class {
|
|
|
3584
3787
|
this.recomputeEdit(incoming.edit.targetClientMsgId);
|
|
3585
3788
|
return;
|
|
3586
3789
|
}
|
|
3790
|
+
if (incoming.envelopeType === "delete" && incoming.delete) {
|
|
3791
|
+
const actorUserId = direction === "outgoing" ? this.backend.selfUserId : senderUser;
|
|
3792
|
+
this.deleteFold.ingest(
|
|
3793
|
+
{
|
|
3794
|
+
targetClientMsgId: incoming.delete.targetClientMsgId,
|
|
3795
|
+
actorUserId,
|
|
3796
|
+
epoch: incoming.epoch,
|
|
3797
|
+
serverSeq: incoming.serverSeq,
|
|
3798
|
+
eventClientMsgId: incoming.clientMsgId
|
|
3799
|
+
},
|
|
3800
|
+
this.authorOfTarget
|
|
3801
|
+
);
|
|
3802
|
+
this.emit();
|
|
3803
|
+
return;
|
|
3804
|
+
}
|
|
3587
3805
|
const incomingClientMsgId = incoming.clientMsgId;
|
|
3588
3806
|
const incomingReplyRef = incoming.replyRef;
|
|
3589
3807
|
let resolvedReplyTo = null;
|
|
@@ -3604,7 +3822,9 @@ var Chat = class {
|
|
|
3604
3822
|
// BEFORE its target — the dangling case — renders the moment the target lands).
|
|
3605
3823
|
reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {},
|
|
3606
3824
|
// Default false; applyEditOverlay below folds any edit that arrived first.
|
|
3607
|
-
edited: false
|
|
3825
|
+
edited: false,
|
|
3826
|
+
// Default false; surfaced() applies the tombstone scrub if a delete folded.
|
|
3827
|
+
isDeleted: false
|
|
3608
3828
|
};
|
|
3609
3829
|
if (incomingClientMsgId && incoming.text !== null) {
|
|
3610
3830
|
this.byClientMsgId.set(incomingClientMsgId, {
|
|
@@ -3615,6 +3835,7 @@ var Chat = class {
|
|
|
3615
3835
|
if (incomingClientMsgId) {
|
|
3616
3836
|
this.seedEditBase(incomingClientMsgId, incoming.text, senderUser);
|
|
3617
3837
|
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3838
|
+
this.deleteFold.reevaluatePending(incomingClientMsgId, senderUser);
|
|
3618
3839
|
}
|
|
3619
3840
|
this.messageList.push(this.applyEditOverlay(msg));
|
|
3620
3841
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
@@ -3856,7 +4077,9 @@ var Chat = class {
|
|
|
3856
4077
|
// the dangling-target invariant uniform across every append path).
|
|
3857
4078
|
reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {},
|
|
3858
4079
|
// Own-sent edits fold via edit() after the fact; new sends start unedited.
|
|
3859
|
-
edited: false
|
|
4080
|
+
edited: false,
|
|
4081
|
+
// Own-sent deletes fold via deleteForEveryone() after the fact; start live.
|
|
4082
|
+
isDeleted: false
|
|
3860
4083
|
});
|
|
3861
4084
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
3862
4085
|
this.emit();
|
|
@@ -3968,6 +4191,52 @@ var Chat = class {
|
|
|
3968
4191
|
);
|
|
3969
4192
|
this.recomputeEdit(message.clientMsgId);
|
|
3970
4193
|
}
|
|
4194
|
+
// ── Delete ──
|
|
4195
|
+
/** Delete a message for EVERYONE (a cooperative encrypted tombstone). Only the
|
|
4196
|
+
* ORIGINAL SENDER can do this — for an own message self IS the author, so the
|
|
4197
|
+
* author-gate passes. Legacy messages (empty clientMsgId) are DISABLED (a
|
|
4198
|
+
* tombstone keys on the target's clientMsgId, which they lack) — no-op. Sends a
|
|
4199
|
+
* `type:'delete'` envelope through the SAME MLS path as a text message (the
|
|
4200
|
+
* server stays blind), folds the own delete locally so the target scrubs in
|
|
4201
|
+
* place instantly (the durable echo dedups on the SAME wire clientMsgId), and
|
|
4202
|
+
* re-emits. NEVER appends a bubble. delete-for-me'ing the target becomes moot. */
|
|
4203
|
+
async deleteForEveryone(message) {
|
|
4204
|
+
if (!message.clientMsgId) return;
|
|
4205
|
+
const group = await this.materializeIfNeeded();
|
|
4206
|
+
const clientMsgId = mintClientMsgId();
|
|
4207
|
+
const { receipt } = await this.backend.sendDelete(group, {
|
|
4208
|
+
clientMsgId,
|
|
4209
|
+
targetClientMsgId: message.clientMsgId
|
|
4210
|
+
});
|
|
4211
|
+
this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
|
|
4212
|
+
this.deleteFold.ingest(
|
|
4213
|
+
{
|
|
4214
|
+
targetClientMsgId: message.clientMsgId,
|
|
4215
|
+
actorUserId: this.backend.selfUserId,
|
|
4216
|
+
epoch: receipt.epoch,
|
|
4217
|
+
serverSeq: receipt.serverSeq,
|
|
4218
|
+
eventClientMsgId: clientMsgId
|
|
4219
|
+
},
|
|
4220
|
+
this.authorOfTarget
|
|
4221
|
+
);
|
|
4222
|
+
this.emit();
|
|
4223
|
+
}
|
|
4224
|
+
/** Delete a message for ME only — a LOCAL, per-device suppression. NO wire, NO
|
|
4225
|
+
* attribution, no server contact: the message is OMITTED from THIS view and the
|
|
4226
|
+
* suppression key persists per chat (survives reload). The key is the message's
|
|
4227
|
+
* clientMsgId when present, else `seq:<serverSeq>` for legacy messages. */
|
|
4228
|
+
async deleteForMe(message) {
|
|
4229
|
+
const key = message.clientMsgId ? message.clientMsgId : `seq:${message.serverSeq}`;
|
|
4230
|
+
if (this.suppressed.has(key)) return;
|
|
4231
|
+
this.suppressed.add(key);
|
|
4232
|
+
this.emit();
|
|
4233
|
+
if (this._group) {
|
|
4234
|
+
try {
|
|
4235
|
+
await this.backend.saveSuppressed(this._group, [...this.suppressed]);
|
|
4236
|
+
} catch {
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
3971
4240
|
};
|
|
3972
4241
|
function sameReactions(a, b) {
|
|
3973
4242
|
const ak = Object.keys(a);
|
|
@@ -4148,6 +4417,7 @@ var MessageDeliverySource = class {
|
|
|
4148
4417
|
const { text, clientMsgId, replyTo } = decoded;
|
|
4149
4418
|
const isReaction = decoded.type === "reaction" && decoded.reaction != null;
|
|
4150
4419
|
const isEdit = decoded.type === "edit" && decoded.edit != null;
|
|
4420
|
+
const isDelete = decoded.type === "delete" && decoded.delete != null;
|
|
4151
4421
|
const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
|
|
4152
4422
|
const stored = {
|
|
4153
4423
|
id: `${group.rfcGroupId}#${row.server_seq}`,
|
|
@@ -4186,6 +4456,18 @@ var MessageDeliverySource = class {
|
|
|
4186
4456
|
targetClientMsgId: decoded.edit.targetClientMsgId,
|
|
4187
4457
|
newText: decoded.edit.newText
|
|
4188
4458
|
}
|
|
4459
|
+
} : {},
|
|
4460
|
+
// Thread the delete discriminator + target through the persisted row so a
|
|
4461
|
+
// delete-for-everyone tombstone folded LIVE re-folds onto its target after
|
|
4462
|
+
// a reload (the reload-parity boundary — the iOS-review CRITICAL lesson;
|
|
4463
|
+
// the projection's `.delete` branch re-folds it so it never leaks a blank
|
|
4464
|
+
// bubble). Omitted for non-deletes → old rows hydrate as `'text'`/no-delete.
|
|
4465
|
+
...isDelete && decoded.delete ? {
|
|
4466
|
+
envelopeType: "delete",
|
|
4467
|
+
delete: {
|
|
4468
|
+
targetClientMsgId: decoded.delete.targetClientMsgId,
|
|
4469
|
+
scope: decoded.delete.scope
|
|
4470
|
+
}
|
|
4189
4471
|
} : {}
|
|
4190
4472
|
};
|
|
4191
4473
|
try {
|
|
@@ -4205,7 +4487,8 @@ var MessageDeliverySource = class {
|
|
|
4205
4487
|
replyRef: replyTo,
|
|
4206
4488
|
envelopeType: decoded.type ?? "text",
|
|
4207
4489
|
reaction: isReaction ? decoded.reaction : null,
|
|
4208
|
-
edit: isEdit ? decoded.edit : null
|
|
4490
|
+
edit: isEdit ? decoded.edit : null,
|
|
4491
|
+
delete: isDelete ? decoded.delete : null
|
|
4209
4492
|
});
|
|
4210
4493
|
return true;
|
|
4211
4494
|
}
|
|
@@ -6027,6 +6310,36 @@ var SignatureKeyStore = class {
|
|
|
6027
6310
|
}
|
|
6028
6311
|
};
|
|
6029
6312
|
|
|
6313
|
+
// src/messaging/suppression.ts
|
|
6314
|
+
var SuppressionStore = class {
|
|
6315
|
+
constructor(kv) {
|
|
6316
|
+
this.kv = kv;
|
|
6317
|
+
}
|
|
6318
|
+
kv;
|
|
6319
|
+
key(rfcGroupId) {
|
|
6320
|
+
return `supp:${rfcGroupId}`;
|
|
6321
|
+
}
|
|
6322
|
+
/** Load the persisted suppression keys for a chat (empty array if none). */
|
|
6323
|
+
async load(rfcGroupId) {
|
|
6324
|
+
const raw = await this.kv.get(this.key(rfcGroupId));
|
|
6325
|
+
if (!raw) return [];
|
|
6326
|
+
try {
|
|
6327
|
+
const parsed = JSON.parse(decodeUtf8(raw));
|
|
6328
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
6329
|
+
} catch {
|
|
6330
|
+
return [];
|
|
6331
|
+
}
|
|
6332
|
+
}
|
|
6333
|
+
/** Persist the full suppression key set for a chat (deterministic order). */
|
|
6334
|
+
async save(rfcGroupId, keys) {
|
|
6335
|
+
const sorted = [...new Set(keys)].sort();
|
|
6336
|
+
await this.kv.set(this.key(rfcGroupId), encodeUtf8(JSON.stringify(sorted)));
|
|
6337
|
+
}
|
|
6338
|
+
async wipe() {
|
|
6339
|
+
for (const k of await this.kv.keys("supp:")) await this.kv.delete(k);
|
|
6340
|
+
}
|
|
6341
|
+
};
|
|
6342
|
+
|
|
6030
6343
|
// src/messaging/coordinator.ts
|
|
6031
6344
|
var MessagingCoordinator = class {
|
|
6032
6345
|
constructor(rt) {
|
|
@@ -6036,6 +6349,7 @@ var MessagingCoordinator = class {
|
|
|
6036
6349
|
this.sigStore = new SignatureKeyStore(this.kv);
|
|
6037
6350
|
this.groupStore = new GroupStateStorage(this.kv);
|
|
6038
6351
|
this.kpStore = new KeyPackageStorage(this.kv);
|
|
6352
|
+
this.suppressionStore = new SuppressionStore(this.kv);
|
|
6039
6353
|
this.registry.attachChatList(
|
|
6040
6354
|
(chats) => {
|
|
6041
6355
|
this.chatList = chats;
|
|
@@ -6050,6 +6364,7 @@ var MessagingCoordinator = class {
|
|
|
6050
6364
|
sigStore;
|
|
6051
6365
|
groupStore;
|
|
6052
6366
|
kpStore;
|
|
6367
|
+
suppressionStore;
|
|
6053
6368
|
registry = new GroupRegistry();
|
|
6054
6369
|
resolved = null;
|
|
6055
6370
|
resolvePromise = null;
|
|
@@ -6217,6 +6532,18 @@ var MessagingCoordinator = class {
|
|
|
6217
6532
|
const r = await this.resolve();
|
|
6218
6533
|
return r.groups.sendEdit(group, args);
|
|
6219
6534
|
}
|
|
6535
|
+
async sendDelete(group, args) {
|
|
6536
|
+
const r = await this.resolve();
|
|
6537
|
+
return r.groups.sendDelete(group, args);
|
|
6538
|
+
}
|
|
6539
|
+
/** Load this chat's persisted delete-for-me suppression keys (durable-only). */
|
|
6540
|
+
loadSuppressed(group) {
|
|
6541
|
+
return this.suppressionStore.load(group.rfcGroupId);
|
|
6542
|
+
}
|
|
6543
|
+
/** Persist this chat's delete-for-me suppression keys (durable-only, no wire). */
|
|
6544
|
+
saveSuppressed(group, keys) {
|
|
6545
|
+
return this.suppressionStore.save(group.rfcGroupId, keys);
|
|
6546
|
+
}
|
|
6220
6547
|
async history(group, limit, before) {
|
|
6221
6548
|
const r = await this.resolve();
|
|
6222
6549
|
const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
|
|
@@ -6314,9 +6641,11 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6314
6641
|
});
|
|
6315
6642
|
}
|
|
6316
6643
|
const editFold = new EditFold();
|
|
6644
|
+
const deleteFold = new DeleteFold();
|
|
6317
6645
|
const authorByClientMsgId = /* @__PURE__ */ new Map();
|
|
6318
6646
|
for (const s of rows) {
|
|
6319
|
-
if (s.envelopeType === "reaction" || s.envelopeType === "edit")
|
|
6647
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit" || s.envelopeType === "delete")
|
|
6648
|
+
continue;
|
|
6320
6649
|
const cid = s.clientMsgId ?? "";
|
|
6321
6650
|
if (!cid) continue;
|
|
6322
6651
|
const author = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null);
|
|
@@ -6339,9 +6668,25 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6339
6668
|
);
|
|
6340
6669
|
}
|
|
6341
6670
|
editFold.reevaluateHeld(authorOfTarget);
|
|
6671
|
+
for (const s of rows) {
|
|
6672
|
+
if (s.envelopeType !== "delete" || !s.delete) continue;
|
|
6673
|
+
const actor = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null) ?? null;
|
|
6674
|
+
deleteFold.ingest(
|
|
6675
|
+
{
|
|
6676
|
+
targetClientMsgId: s.delete.targetClientMsgId,
|
|
6677
|
+
actorUserId: actor,
|
|
6678
|
+
epoch: s.epoch,
|
|
6679
|
+
serverSeq: s.serverSeq,
|
|
6680
|
+
eventClientMsgId: s.clientMsgId ?? `${s.id}`
|
|
6681
|
+
},
|
|
6682
|
+
authorOfTarget
|
|
6683
|
+
);
|
|
6684
|
+
}
|
|
6685
|
+
for (const [cid, author] of authorByClientMsgId) deleteFold.reevaluatePending(cid, author);
|
|
6342
6686
|
const lookup = /* @__PURE__ */ new Map();
|
|
6343
6687
|
for (const s of rows) {
|
|
6344
|
-
if (s.envelopeType === "reaction")
|
|
6688
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit" || s.envelopeType === "delete")
|
|
6689
|
+
continue;
|
|
6345
6690
|
const cid = s.clientMsgId ?? "";
|
|
6346
6691
|
if (cid && s.text !== null) {
|
|
6347
6692
|
const senderUserId = s.direction === "outgoing" ? selfUserId : "";
|
|
@@ -6350,8 +6695,27 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6350
6695
|
}
|
|
6351
6696
|
const out = [];
|
|
6352
6697
|
for (const s of rows) {
|
|
6353
|
-
if (s.envelopeType === "reaction" || s.envelopeType === "edit")
|
|
6698
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit" || s.envelopeType === "delete")
|
|
6699
|
+
continue;
|
|
6354
6700
|
const clientMsgId = s.clientMsgId ?? "";
|
|
6701
|
+
const isDeleted = clientMsgId ? deleteFold.isTombstoned(clientMsgId) : false;
|
|
6702
|
+
if (isDeleted) {
|
|
6703
|
+
out.push({
|
|
6704
|
+
id: `${displayId}#${s.serverSeq}`,
|
|
6705
|
+
kind: "text",
|
|
6706
|
+
direction: s.direction,
|
|
6707
|
+
senderUserId: s.direction === "outgoing" ? selfUserId : null,
|
|
6708
|
+
text: DELETED_DESCRIPTOR,
|
|
6709
|
+
serverSeq: s.serverSeq,
|
|
6710
|
+
sentAt: new Date(s.at),
|
|
6711
|
+
clientMsgId,
|
|
6712
|
+
replyTo: null,
|
|
6713
|
+
reactions: {},
|
|
6714
|
+
edited: false,
|
|
6715
|
+
isDeleted: true
|
|
6716
|
+
});
|
|
6717
|
+
continue;
|
|
6718
|
+
}
|
|
6355
6719
|
let replyTo = null;
|
|
6356
6720
|
if (s.replyTo) {
|
|
6357
6721
|
const ref = {
|
|
@@ -6379,7 +6743,8 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6379
6743
|
clientMsgId,
|
|
6380
6744
|
replyTo,
|
|
6381
6745
|
reactions: clientMsgId ? fold.tally(clientMsgId) : {},
|
|
6382
|
-
edited
|
|
6746
|
+
edited,
|
|
6747
|
+
isDeleted: false
|
|
6383
6748
|
});
|
|
6384
6749
|
}
|
|
6385
6750
|
return out;
|
|
@@ -7117,7 +7482,7 @@ function defaultSessionStorage(key) {
|
|
|
7117
7482
|
}
|
|
7118
7483
|
|
|
7119
7484
|
// src/version.ts
|
|
7120
|
-
var VERSION = "1.
|
|
7485
|
+
var VERSION = "1.4.0";
|
|
7121
7486
|
|
|
7122
7487
|
// src/runtime.ts
|
|
7123
7488
|
function buildRuntime(config) {
|