@palbase/web 1.5.0 → 1.6.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.
@@ -700,6 +700,24 @@ declare class PalbeFlags {
700
700
  destroy(): void;
701
701
  }
702
702
 
703
+ /**
704
+ * Pure deadline math — the web mirror of iOS DeadlineCalculator (Codex-finalized
705
+ * restart conversion rule). Identical algorithm; web uses ms internally and
706
+ * compares against ttlSeconds (converted). Clock readings are passed in so it is
707
+ * fully table-testable and byte-identical with iOS.
708
+ *
709
+ * NOTE on the send-vs-arrival clamp: like iOS T3, this Calculator stays PURE — it
710
+ * consumes an already-resolved anchor + the current clock readings. The
711
+ * `min(sender_send_ts + ttl, first_arrival_monotonic + ttl)` effective-deadline
712
+ * basis is applied at ARM time by the caller (Chat, T10), which picks the anchor's
713
+ * monotonic baseline; the Calculator only converts that anchor into remaining/purge.
714
+ */
715
+ interface AnchorTriple {
716
+ mAnchorMs: number;
717
+ wAnchorEpochMs: number;
718
+ bAnchorToken: string;
719
+ }
720
+
703
721
  /** A direct (1:1) chat or a multi-party group. */
704
722
  type ChatKind = 'direct' | 'group';
705
723
  /** A draft (local-only, not yet on the server) or an active (materialized) chat. */
@@ -797,6 +815,16 @@ interface ChatMessage {
797
815
  * DOMINATES); when it is edited they reflect the EDIT's replacement `body_ranges`.
798
816
  */
799
817
  readonly mentions: ResolvedMention[];
818
+ /**
819
+ * The LOCAL purge deadline for this message (monotonic-derived, never a wire field).
820
+ * `null` when this message has no TTL. The UI may show a countdown; the durable truth
821
+ * is the persisted anchor + a re-check on every load (disappearing T10). A message's
822
+ * effective TTL is its OWN per-message `expiry` if present, else the chat default
823
+ * `timer_set` active AS OF this message's order (forward-only — a message ordered
824
+ * BEFORE the governing `timer_set` inherits nothing; own-expiry WINS over the default).
825
+ * Mirrors iOS `ChatMessage.effectiveExpiry` (which it derives from).
826
+ */
827
+ readonly expiresAt: Date | null;
800
828
  }
801
829
  /** The receipt from a send — the server's monotonic sequence + accepted epoch. */
802
830
  interface SentReceipt {
@@ -859,6 +887,13 @@ interface DeletePayload {
859
887
  targetClientMsgId: string;
860
888
  scope: string;
861
889
  }
890
+ /** Per-message TTL spec (decoded). senderSendTs present iff start === 'send'. */
891
+ interface ExpirySpec {
892
+ v: number;
893
+ ttlSeconds: number;
894
+ start: 'send' | 'read';
895
+ senderSendTs: number | null;
896
+ }
862
897
 
863
898
  /** A decoded incoming message handed to chat listeners. */
864
899
  interface IncomingMessage {
@@ -893,6 +928,16 @@ interface IncomingMessage {
893
928
  * with `body_ranges` OR on an `'edit'` (the edit's replacement ranges). The Chat
894
929
  * normalizes + resolves roster names → `ChatMessage.mentions` (mentions T6). */
895
930
  bodyRanges?: MentionRange[] | null;
931
+ /** The decoded `timer_set` payload, present only when `envelopeType === 'timer_set'`
932
+ * (disappearing T10). Lets the Chat route a chat-default timer into its TimerFold
933
+ * instead of appending a visible bubble. */
934
+ timer?: {
935
+ ttlSeconds: number | null;
936
+ start: 'send' | 'read';
937
+ } | null;
938
+ /** The decoded per-message TTL spec, present only on a `'text'` bubble that carried an
939
+ * `expiry` (disappearing T10). Lets the Chat arm the message's purge on first display. */
940
+ expiry?: ExpirySpec | null;
896
941
  }
897
942
 
898
943
  /** The intended participants of a draft chat (held until the first send). */
@@ -912,7 +957,7 @@ interface ChatBackend {
912
957
  selfUserId: string;
913
958
  /** Materialize a draft → the active group (DM get-or-create / group create+add). */
914
959
  materialize(draft: ChatDraft): Promise<MessagingGroup>;
915
- sendText(group: MessagingGroup, text: string, replyTo?: ReplyRef | null, bodyRanges?: MentionRange[] | null): Promise<{
960
+ sendText(group: MessagingGroup, text: string, replyTo?: ReplyRef | null, bodyRanges?: MentionRange[] | null, expiry?: ExpirySpec | null): Promise<{
916
961
  receipt: SentReceipt;
917
962
  clientMsgId: string;
918
963
  }>;
@@ -969,6 +1014,31 @@ interface ChatBackend {
969
1014
  loadElevated(group: MessagingGroup): Promise<string[]>;
970
1015
  /** Persist this chat's self-elevation dedup keys (durable, no wire). */
971
1016
  saveElevated(group: MessagingGroup, keys: string[]): Promise<void>;
1017
+ /** Send a per-chat default disappearing-timer control envelope (`timer_set`). Goes
1018
+ * through the SAME MLS application path as `sendText` — server-blind, never a bubble.
1019
+ * `ttlSeconds === null` disables the default. Returns the receipt + the wire clientMsgId. */
1020
+ sendTimerSet(group: MessagingGroup, args: {
1021
+ clientMsgId: string;
1022
+ ttlSeconds: number | null;
1023
+ start: 'send' | 'read';
1024
+ }): Promise<{
1025
+ receipt: SentReceipt;
1026
+ clientMsgId: string;
1027
+ }>;
1028
+ /** The persisted INTEGER `server_seq` tombstone set for a chat — drives transcript
1029
+ * exclusion (cold-launch) + redelivery drop (live arrival). Durable, no wire. */
1030
+ tombstonedSeqs(group: MessagingGroup): Promise<Set<number>>;
1031
+ /** The persisted STRING `client_msg_id` purge set — drives the orphan-fold resolver
1032
+ * (`authorOfTarget → 'purged'`) so a late edit DROPs / a late delete no-ops. */
1033
+ purgedClientMsgIds(group: MessagingGroup): Promise<Set<string>>;
1034
+ /** The persisted write-once anchor triple for a `clientMsgId`, or null if none. */
1035
+ anchor(group: MessagingGroup, clientMsgId: string): Promise<AnchorTriple | null>;
1036
+ /** Write-once: capture the first-arrival/first-read anchor triple for a `clientMsgId`
1037
+ * (a second call for the same id is a no-op, so the deadline never resets). */
1038
+ writeAnchorOnce(group: MessagingGroup, clientMsgId: string, a: AnchorTriple): Promise<void>;
1039
+ /** Tombstone-first purge commit point: persist the int `server_seq` + string
1040
+ * `client_msg_id` purge id in ONE durable record (idempotent). */
1041
+ tombstone(group: MessagingGroup, serverSeq: number, clientMsgId: string): Promise<void>;
972
1042
  }
973
1043
  declare class Chat {
974
1044
  /** Stable, URL/log-safe id (the grp_ display id once active; a reserved local id while draft). */
@@ -994,6 +1064,22 @@ declare class Chat {
994
1064
  /** The single authoritative delete-for-everyone fold (live + own-send + history).
995
1065
  * A tombstone scrubs its target in place (delete DOMINATES edit at render). */
996
1066
  private readonly deleteFold;
1067
+ /** The per-chat default disappearing-timer fold — the latest valid `timer_set` (LWW
1068
+ * on (epoch, serverSeq), author = the resolved MLS sender). A `timer_set` is NEVER a
1069
+ * bubble; it routes here. The active default governs a subsequent bubble that carries
1070
+ * no per-message expiry (disappearing T10). */
1071
+ private readonly timerFold;
1072
+ /** Advisory in-memory purge timers, keyed by serverSeq. The DURABLE truth is the
1073
+ * persisted anchor + a re-check on every load; this just drives live eviction while
1074
+ * the tab is open. Cancelled when the message purges (disappearing T10). */
1075
+ private readonly purgeTimers;
1076
+ /** In-memory mirror of the durable `purgedClientMsgIds` set (the STRING namespace),
1077
+ * hydrated from `backend.purgedClientMsgIds` and grown by each live purge. Consulted
1078
+ * by `authorOfTarget` so a late edit/delete targeting a TTL-purged message resolves to
1079
+ * `'purged'` (DROP / no-op — never resurrects). Namespace-separate from the int seq
1080
+ * tombstone (disappearing T10). */
1081
+ private readonly purgedCids;
1082
+ private purgedLoaded;
997
1083
  /** delete-for-me suppression keys (clientMsgId, or `seq:<serverSeq>` for legacy)
998
1084
  * — the message is OMITTED from this view. Local + persisted per chat, NO wire. */
999
1085
  private readonly suppressed;
@@ -1063,6 +1149,10 @@ declare class Chat {
1063
1149
  get title(): string;
1064
1150
  presence(userId: string): PresenceState | null;
1065
1151
  private ensureWired;
1152
+ /** Hydrate the durable `purgedClientMsgIds` set (once) into the in-memory mirror so the
1153
+ * live Edit/Delete fold author-gate sees TTL-purged targets as 'purged' on cold launch
1154
+ * (disappearing T10). No re-emit: it only gates the orphan-fold resolution. */
1155
+ private loadPurged;
1066
1156
  /** Hydrate the persisted delete-for-me suppression keys (once), then re-emit so
1067
1157
  * any already-surfaced suppressed message is omitted (cold-launch parity). */
1068
1158
  private loadSuppressed;
@@ -1073,9 +1163,45 @@ declare class Chat {
1073
1163
  private mergeHistory;
1074
1164
  /** @internal — called by the backend's live subscription. */
1075
1165
  ingestLive(incoming: IncomingMessage): Promise<void>;
1076
- /** The EditFold author-gate input: the target message's resolved author userId
1077
- * (null = target unknown/dangling the fold HOLDs). Captured as a bound arrow
1078
- * so it can be passed to the pure EditFold. */
1166
+ /** Normalize a decoded `IncomingMessage.expiry` / `StoredMessage.expiry` into the
1167
+ * `ExpirySpec` the arm path consumes (or null when absent). */
1168
+ private toExpirySpec;
1169
+ /** The chat-default expiry derived from the active `timer_set` fold, as an `ExpirySpec`
1170
+ * so a bubble with no per-message expiry inherits it. null when no default is active or
1171
+ * the default was explicitly DISABLED (`ttlSeconds === null`). `senderSendTs` is null —
1172
+ * the default has no per-message sender clock; the arrival anchor drives the deadline
1173
+ * (mirrors iOS `defaultExpiry()`). */
1174
+ private defaultExpiry;
1175
+ /** The surfaced display deadline for an effective expiry (a local, monotonic-derived
1176
+ * value — the wall-clock projection of the TTL from now). null for a non-disappearing
1177
+ * message. The durable purge is driven by `armPurge`'s write-once anchor; this is the
1178
+ * UI countdown baseline. */
1179
+ private deadlineFor;
1180
+ /** Arm a message's TTL purge on first decrypt-and-display. Captures the WRITE-ONCE
1181
+ * monotonic/wall/boot anchor (so the deadline survives a reload — a re-arm after relaunch
1182
+ * reads back the ORIGINAL capture, never a fresh one → the deadline never resets),
1183
+ * computes the remaining time via `remainingSeconds`, applies the send-anchor clamp
1184
+ * `min(sender_send_ts+ttl, first_arrival+ttl)` (read-anchor uses the write-once first-read
1185
+ * capture), then either purges immediately or schedules an advisory `setTimeout`. A null
1186
+ * expiry / empty clientMsgId is a no-op. Mirrors iOS `armPurge`. */
1187
+ private armPurge;
1188
+ /** Re-arm a purge from a derived deadline (cold-launch hydrate path). The deadline is
1189
+ * the projection's monotonic-derived `expiresAt`; schedule an advisory timer for the
1190
+ * remaining time (purge immediately if the deadline has already passed). The durable
1191
+ * tombstone is written by `purge` when it fires (the crash-safe commit point). */
1192
+ private armFromDeadline;
1193
+ /** Purge message M (TTL eviction). TOMBSTONE-FIRST (the crash-safe commit point):
1194
+ * persist the `server_seq` tombstone + the `client_msg_id` purge id in ONE durable
1195
+ * record, THEN drop M's body from `messageList` + `emit()`, then re-evaluate any HELD
1196
+ * edit / PARKED delete targeting the now-purged cid so an orphan annotation DROPs/no-ops
1197
+ * (the resolver now returns `'purged'`). Idempotent. Mirrors iOS `purge`. */
1198
+ private purge;
1199
+ /** The Edit/Delete fold author-gate input via {@link AuthorResolution} (disappearing
1200
+ * T10 — the orphan-aware resolver): `'purged'` when the target's clientMsgId is in the
1201
+ * durable purge set (a late edit DROPs / a late delete no-ops — never resurrects a
1202
+ * disappeared message); `'author'` when its author is locally known → run the
1203
+ * author-gate; `'unknown'` otherwise → HOLD. The live twin of `projectHistory`'s
1204
+ * resolver. Captured as a bound arrow so it can be passed to the pure folds. */
1079
1205
  private readonly authorOfTarget;
1080
1206
  /** Resolve a message's decode-time mention spans: NORMALIZE the raw `body_ranges`
1081
1207
  * against `text` (the pinned `normalizeMentionRangesUtf16` cross-SDK contract) then
@@ -1150,7 +1276,21 @@ declare class Chat {
1150
1276
  send(text: string, opts?: {
1151
1277
  replyTo?: ChatMessage;
1152
1278
  mentions?: MentionRange[];
1279
+ expiresIn?: {
1280
+ ttlSeconds: number;
1281
+ start?: 'send' | 'read';
1282
+ };
1153
1283
  }): Promise<SentReceipt>;
1284
+ /** Set (or DISABLE) this chat's DEFAULT disappearing timer. Emits a `timer_set` control
1285
+ * envelope (server-blind — an opaque application message, NEVER a bubble) and folds the
1286
+ * own-set locally so the default applies immediately to subsequent sends that carry no
1287
+ * per-message expiry. `ttlSeconds === null` DISABLES the default. FIRE-AND-FORGET
1288
+ * ADVISORY: returns on the LOCAL emit only; it exposes NO "active for all peers" signal.
1289
+ * Mirrors iOS `setDisappearing(ttlSeconds:start:)`. */
1290
+ setDisappearing(opts: {
1291
+ ttlSeconds: number | null;
1292
+ start?: 'send' | 'read';
1293
+ }): Promise<void>;
1154
1294
  private appendOwnSend;
1155
1295
  private materializeIfNeeded;
1156
1296
  addMemberUser(userId: string): Promise<void>;
@@ -700,6 +700,24 @@ declare class PalbeFlags {
700
700
  destroy(): void;
701
701
  }
702
702
 
703
+ /**
704
+ * Pure deadline math — the web mirror of iOS DeadlineCalculator (Codex-finalized
705
+ * restart conversion rule). Identical algorithm; web uses ms internally and
706
+ * compares against ttlSeconds (converted). Clock readings are passed in so it is
707
+ * fully table-testable and byte-identical with iOS.
708
+ *
709
+ * NOTE on the send-vs-arrival clamp: like iOS T3, this Calculator stays PURE — it
710
+ * consumes an already-resolved anchor + the current clock readings. The
711
+ * `min(sender_send_ts + ttl, first_arrival_monotonic + ttl)` effective-deadline
712
+ * basis is applied at ARM time by the caller (Chat, T10), which picks the anchor's
713
+ * monotonic baseline; the Calculator only converts that anchor into remaining/purge.
714
+ */
715
+ interface AnchorTriple {
716
+ mAnchorMs: number;
717
+ wAnchorEpochMs: number;
718
+ bAnchorToken: string;
719
+ }
720
+
703
721
  /** A direct (1:1) chat or a multi-party group. */
704
722
  type ChatKind = 'direct' | 'group';
705
723
  /** A draft (local-only, not yet on the server) or an active (materialized) chat. */
@@ -797,6 +815,16 @@ interface ChatMessage {
797
815
  * DOMINATES); when it is edited they reflect the EDIT's replacement `body_ranges`.
798
816
  */
799
817
  readonly mentions: ResolvedMention[];
818
+ /**
819
+ * The LOCAL purge deadline for this message (monotonic-derived, never a wire field).
820
+ * `null` when this message has no TTL. The UI may show a countdown; the durable truth
821
+ * is the persisted anchor + a re-check on every load (disappearing T10). A message's
822
+ * effective TTL is its OWN per-message `expiry` if present, else the chat default
823
+ * `timer_set` active AS OF this message's order (forward-only — a message ordered
824
+ * BEFORE the governing `timer_set` inherits nothing; own-expiry WINS over the default).
825
+ * Mirrors iOS `ChatMessage.effectiveExpiry` (which it derives from).
826
+ */
827
+ readonly expiresAt: Date | null;
800
828
  }
801
829
  /** The receipt from a send — the server's monotonic sequence + accepted epoch. */
802
830
  interface SentReceipt {
@@ -859,6 +887,13 @@ interface DeletePayload {
859
887
  targetClientMsgId: string;
860
888
  scope: string;
861
889
  }
890
+ /** Per-message TTL spec (decoded). senderSendTs present iff start === 'send'. */
891
+ interface ExpirySpec {
892
+ v: number;
893
+ ttlSeconds: number;
894
+ start: 'send' | 'read';
895
+ senderSendTs: number | null;
896
+ }
862
897
 
863
898
  /** A decoded incoming message handed to chat listeners. */
864
899
  interface IncomingMessage {
@@ -893,6 +928,16 @@ interface IncomingMessage {
893
928
  * with `body_ranges` OR on an `'edit'` (the edit's replacement ranges). The Chat
894
929
  * normalizes + resolves roster names → `ChatMessage.mentions` (mentions T6). */
895
930
  bodyRanges?: MentionRange[] | null;
931
+ /** The decoded `timer_set` payload, present only when `envelopeType === 'timer_set'`
932
+ * (disappearing T10). Lets the Chat route a chat-default timer into its TimerFold
933
+ * instead of appending a visible bubble. */
934
+ timer?: {
935
+ ttlSeconds: number | null;
936
+ start: 'send' | 'read';
937
+ } | null;
938
+ /** The decoded per-message TTL spec, present only on a `'text'` bubble that carried an
939
+ * `expiry` (disappearing T10). Lets the Chat arm the message's purge on first display. */
940
+ expiry?: ExpirySpec | null;
896
941
  }
897
942
 
898
943
  /** The intended participants of a draft chat (held until the first send). */
@@ -912,7 +957,7 @@ interface ChatBackend {
912
957
  selfUserId: string;
913
958
  /** Materialize a draft → the active group (DM get-or-create / group create+add). */
914
959
  materialize(draft: ChatDraft): Promise<MessagingGroup>;
915
- sendText(group: MessagingGroup, text: string, replyTo?: ReplyRef | null, bodyRanges?: MentionRange[] | null): Promise<{
960
+ sendText(group: MessagingGroup, text: string, replyTo?: ReplyRef | null, bodyRanges?: MentionRange[] | null, expiry?: ExpirySpec | null): Promise<{
916
961
  receipt: SentReceipt;
917
962
  clientMsgId: string;
918
963
  }>;
@@ -969,6 +1014,31 @@ interface ChatBackend {
969
1014
  loadElevated(group: MessagingGroup): Promise<string[]>;
970
1015
  /** Persist this chat's self-elevation dedup keys (durable, no wire). */
971
1016
  saveElevated(group: MessagingGroup, keys: string[]): Promise<void>;
1017
+ /** Send a per-chat default disappearing-timer control envelope (`timer_set`). Goes
1018
+ * through the SAME MLS application path as `sendText` — server-blind, never a bubble.
1019
+ * `ttlSeconds === null` disables the default. Returns the receipt + the wire clientMsgId. */
1020
+ sendTimerSet(group: MessagingGroup, args: {
1021
+ clientMsgId: string;
1022
+ ttlSeconds: number | null;
1023
+ start: 'send' | 'read';
1024
+ }): Promise<{
1025
+ receipt: SentReceipt;
1026
+ clientMsgId: string;
1027
+ }>;
1028
+ /** The persisted INTEGER `server_seq` tombstone set for a chat — drives transcript
1029
+ * exclusion (cold-launch) + redelivery drop (live arrival). Durable, no wire. */
1030
+ tombstonedSeqs(group: MessagingGroup): Promise<Set<number>>;
1031
+ /** The persisted STRING `client_msg_id` purge set — drives the orphan-fold resolver
1032
+ * (`authorOfTarget → 'purged'`) so a late edit DROPs / a late delete no-ops. */
1033
+ purgedClientMsgIds(group: MessagingGroup): Promise<Set<string>>;
1034
+ /** The persisted write-once anchor triple for a `clientMsgId`, or null if none. */
1035
+ anchor(group: MessagingGroup, clientMsgId: string): Promise<AnchorTriple | null>;
1036
+ /** Write-once: capture the first-arrival/first-read anchor triple for a `clientMsgId`
1037
+ * (a second call for the same id is a no-op, so the deadline never resets). */
1038
+ writeAnchorOnce(group: MessagingGroup, clientMsgId: string, a: AnchorTriple): Promise<void>;
1039
+ /** Tombstone-first purge commit point: persist the int `server_seq` + string
1040
+ * `client_msg_id` purge id in ONE durable record (idempotent). */
1041
+ tombstone(group: MessagingGroup, serverSeq: number, clientMsgId: string): Promise<void>;
972
1042
  }
973
1043
  declare class Chat {
974
1044
  /** Stable, URL/log-safe id (the grp_ display id once active; a reserved local id while draft). */
@@ -994,6 +1064,22 @@ declare class Chat {
994
1064
  /** The single authoritative delete-for-everyone fold (live + own-send + history).
995
1065
  * A tombstone scrubs its target in place (delete DOMINATES edit at render). */
996
1066
  private readonly deleteFold;
1067
+ /** The per-chat default disappearing-timer fold — the latest valid `timer_set` (LWW
1068
+ * on (epoch, serverSeq), author = the resolved MLS sender). A `timer_set` is NEVER a
1069
+ * bubble; it routes here. The active default governs a subsequent bubble that carries
1070
+ * no per-message expiry (disappearing T10). */
1071
+ private readonly timerFold;
1072
+ /** Advisory in-memory purge timers, keyed by serverSeq. The DURABLE truth is the
1073
+ * persisted anchor + a re-check on every load; this just drives live eviction while
1074
+ * the tab is open. Cancelled when the message purges (disappearing T10). */
1075
+ private readonly purgeTimers;
1076
+ /** In-memory mirror of the durable `purgedClientMsgIds` set (the STRING namespace),
1077
+ * hydrated from `backend.purgedClientMsgIds` and grown by each live purge. Consulted
1078
+ * by `authorOfTarget` so a late edit/delete targeting a TTL-purged message resolves to
1079
+ * `'purged'` (DROP / no-op — never resurrects). Namespace-separate from the int seq
1080
+ * tombstone (disappearing T10). */
1081
+ private readonly purgedCids;
1082
+ private purgedLoaded;
997
1083
  /** delete-for-me suppression keys (clientMsgId, or `seq:<serverSeq>` for legacy)
998
1084
  * — the message is OMITTED from this view. Local + persisted per chat, NO wire. */
999
1085
  private readonly suppressed;
@@ -1063,6 +1149,10 @@ declare class Chat {
1063
1149
  get title(): string;
1064
1150
  presence(userId: string): PresenceState | null;
1065
1151
  private ensureWired;
1152
+ /** Hydrate the durable `purgedClientMsgIds` set (once) into the in-memory mirror so the
1153
+ * live Edit/Delete fold author-gate sees TTL-purged targets as 'purged' on cold launch
1154
+ * (disappearing T10). No re-emit: it only gates the orphan-fold resolution. */
1155
+ private loadPurged;
1066
1156
  /** Hydrate the persisted delete-for-me suppression keys (once), then re-emit so
1067
1157
  * any already-surfaced suppressed message is omitted (cold-launch parity). */
1068
1158
  private loadSuppressed;
@@ -1073,9 +1163,45 @@ declare class Chat {
1073
1163
  private mergeHistory;
1074
1164
  /** @internal — called by the backend's live subscription. */
1075
1165
  ingestLive(incoming: IncomingMessage): Promise<void>;
1076
- /** The EditFold author-gate input: the target message's resolved author userId
1077
- * (null = target unknown/dangling the fold HOLDs). Captured as a bound arrow
1078
- * so it can be passed to the pure EditFold. */
1166
+ /** Normalize a decoded `IncomingMessage.expiry` / `StoredMessage.expiry` into the
1167
+ * `ExpirySpec` the arm path consumes (or null when absent). */
1168
+ private toExpirySpec;
1169
+ /** The chat-default expiry derived from the active `timer_set` fold, as an `ExpirySpec`
1170
+ * so a bubble with no per-message expiry inherits it. null when no default is active or
1171
+ * the default was explicitly DISABLED (`ttlSeconds === null`). `senderSendTs` is null —
1172
+ * the default has no per-message sender clock; the arrival anchor drives the deadline
1173
+ * (mirrors iOS `defaultExpiry()`). */
1174
+ private defaultExpiry;
1175
+ /** The surfaced display deadline for an effective expiry (a local, monotonic-derived
1176
+ * value — the wall-clock projection of the TTL from now). null for a non-disappearing
1177
+ * message. The durable purge is driven by `armPurge`'s write-once anchor; this is the
1178
+ * UI countdown baseline. */
1179
+ private deadlineFor;
1180
+ /** Arm a message's TTL purge on first decrypt-and-display. Captures the WRITE-ONCE
1181
+ * monotonic/wall/boot anchor (so the deadline survives a reload — a re-arm after relaunch
1182
+ * reads back the ORIGINAL capture, never a fresh one → the deadline never resets),
1183
+ * computes the remaining time via `remainingSeconds`, applies the send-anchor clamp
1184
+ * `min(sender_send_ts+ttl, first_arrival+ttl)` (read-anchor uses the write-once first-read
1185
+ * capture), then either purges immediately or schedules an advisory `setTimeout`. A null
1186
+ * expiry / empty clientMsgId is a no-op. Mirrors iOS `armPurge`. */
1187
+ private armPurge;
1188
+ /** Re-arm a purge from a derived deadline (cold-launch hydrate path). The deadline is
1189
+ * the projection's monotonic-derived `expiresAt`; schedule an advisory timer for the
1190
+ * remaining time (purge immediately if the deadline has already passed). The durable
1191
+ * tombstone is written by `purge` when it fires (the crash-safe commit point). */
1192
+ private armFromDeadline;
1193
+ /** Purge message M (TTL eviction). TOMBSTONE-FIRST (the crash-safe commit point):
1194
+ * persist the `server_seq` tombstone + the `client_msg_id` purge id in ONE durable
1195
+ * record, THEN drop M's body from `messageList` + `emit()`, then re-evaluate any HELD
1196
+ * edit / PARKED delete targeting the now-purged cid so an orphan annotation DROPs/no-ops
1197
+ * (the resolver now returns `'purged'`). Idempotent. Mirrors iOS `purge`. */
1198
+ private purge;
1199
+ /** The Edit/Delete fold author-gate input via {@link AuthorResolution} (disappearing
1200
+ * T10 — the orphan-aware resolver): `'purged'` when the target's clientMsgId is in the
1201
+ * durable purge set (a late edit DROPs / a late delete no-ops — never resurrects a
1202
+ * disappeared message); `'author'` when its author is locally known → run the
1203
+ * author-gate; `'unknown'` otherwise → HOLD. The live twin of `projectHistory`'s
1204
+ * resolver. Captured as a bound arrow so it can be passed to the pure folds. */
1079
1205
  private readonly authorOfTarget;
1080
1206
  /** Resolve a message's decode-time mention spans: NORMALIZE the raw `body_ranges`
1081
1207
  * against `text` (the pinned `normalizeMentionRangesUtf16` cross-SDK contract) then
@@ -1150,7 +1276,21 @@ declare class Chat {
1150
1276
  send(text: string, opts?: {
1151
1277
  replyTo?: ChatMessage;
1152
1278
  mentions?: MentionRange[];
1279
+ expiresIn?: {
1280
+ ttlSeconds: number;
1281
+ start?: 'send' | 'read';
1282
+ };
1153
1283
  }): Promise<SentReceipt>;
1284
+ /** Set (or DISABLE) this chat's DEFAULT disappearing timer. Emits a `timer_set` control
1285
+ * envelope (server-blind — an opaque application message, NEVER a bubble) and folds the
1286
+ * own-set locally so the default applies immediately to subsequent sends that carry no
1287
+ * per-message expiry. `ttlSeconds === null` DISABLES the default. FIRE-AND-FORGET
1288
+ * ADVISORY: returns on the LOCAL emit only; it exposes NO "active for all peers" signal.
1289
+ * Mirrors iOS `setDisappearing(ttlSeconds:start:)`. */
1290
+ setDisappearing(opts: {
1291
+ ttlSeconds: number | null;
1292
+ start?: 'send' | 'read';
1293
+ }): Promise<void>;
1154
1294
  private appendOwnSend;
1155
1295
  private materializeIfNeeded;
1156
1296
  addMemberUser(userId: string): Promise<void>;