@glyphteck/veyl 0.60.0 → 0.61.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/account.js CHANGED
@@ -10287,6 +10287,10 @@ function epochChatSettingsId(epochState) {
10287
10287
  "use client";
10288
10288
  var CHAT_ENTRY_VERSION = 4;
10289
10289
  var CHAT_OWNER_EPOCH_ENTRY_VERSION = 2;
10290
+ var CHAT_OWNER_RETIREMENT_KINDS = Object.freeze({
10291
+ LEAVE: "leave",
10292
+ REMOVED: "removed"
10293
+ });
10290
10294
  function ownChatEntryId(chatPrivateKey, chatId) {
10291
10295
  const key = deriveKey(toBytes32(chatPrivateKey, "chat private key"), "user-chat-entry-id-v4", [cleanChatHex(chatId, "chat id")], 16);
10292
10296
  try {
@@ -10360,6 +10364,20 @@ function normalizeCurrentEpoch(value) {
10360
10364
  settings: normalizeChatSettingsProjection(value.settings)
10361
10365
  };
10362
10366
  }
10367
+ function normalizeOwnerRetirement(value, current) {
10368
+ if (value == null)
10369
+ return null;
10370
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
10371
+ throw new Error("invalid chat owner retirement");
10372
+ }
10373
+ const kind = cleanText(value.kind);
10374
+ const epochVersion = value.epochVersion;
10375
+ if (!Object.values(CHAT_OWNER_RETIREMENT_KINDS).includes(kind) || !Number.isSafeInteger(epochVersion) || epochVersion < current.manifest.epochVersion) {
10376
+ throw new Error("invalid chat owner retirement");
10377
+ }
10378
+ const proposalId = kind === CHAT_OWNER_RETIREMENT_KINDS.LEAVE ? cleanTransitionMessageId(value.proposalId) : null;
10379
+ return { kind, epochVersion, proposalId };
10380
+ }
10363
10381
  function normalizeOwnerEntry(value) {
10364
10382
  if (!value || typeof value !== "object" || Array.isArray(value) || value.v !== CHAT_ENTRY_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
10365
10383
  throw new Error("invalid chat entry");
@@ -10380,7 +10398,8 @@ function normalizeOwnerEntry(value) {
10380
10398
  saved: value.saved || null,
10381
10399
  startMs: Number.isFinite(value.startMs) ? value.startMs : null,
10382
10400
  deliveryRegistered: value.deliveryRegistered === true,
10383
- notificationTag: cleanText(value.notificationTag) || null
10401
+ notificationTag: cleanText(value.notificationTag) || null,
10402
+ retirement: normalizeOwnerRetirement(value.retirement, current)
10384
10403
  };
10385
10404
  }
10386
10405
  async function sealOwnChatEntry(chatPrivateKey, entryId, entry) {
@@ -10421,7 +10440,8 @@ function makeOwnChatEntry(epoch, fields = {}) {
10421
10440
  saved: fields.saved || null,
10422
10441
  startMs: Number.isFinite(fields.startMs) ? fields.startMs : null,
10423
10442
  deliveryRegistered: fields.deliveryRegistered === true,
10424
- notificationTag: cleanText(fields.notificationTag) || null
10443
+ notificationTag: cleanText(fields.notificationTag) || null,
10444
+ retirement: fields.retirement || null
10425
10445
  });
10426
10446
  }
10427
10447
  function normalizeOwnerEpochRecord(value) {
@@ -10429,6 +10449,7 @@ function normalizeOwnerEpochRecord(value) {
10429
10449
  throw new Error("invalid owner epoch entry");
10430
10450
  }
10431
10451
  const manifest = normalizeEpochManifest(value.manifest);
10452
+ const leaveProposalId = value.leaveProposalId == null ? null : cleanTransitionMessageId(value.leaveProposalId);
10432
10453
  return {
10433
10454
  v: CHAT_OWNER_EPOCH_ENTRY_VERSION,
10434
10455
  protocol: CHAT_PROTOCOL_VERSION,
@@ -10438,8 +10459,9 @@ function normalizeOwnerEpochRecord(value) {
10438
10459
  manifest,
10439
10460
  epochSecret: cleanChatHex(value.epochSecret, "epoch secret"),
10440
10461
  cutoffMs: Number.isFinite(value.cutoffMs) ? value.cutoffMs : null,
10441
- successorTransitionCommitment: cleanTransitionCommitment(value.successorTransitionCommitment),
10442
- successorTransitionMessageId: cleanTransitionMessageId(value.successorTransitionMessageId)
10462
+ successorTransitionCommitment: leaveProposalId ? null : cleanTransitionCommitment(value.successorTransitionCommitment),
10463
+ successorTransitionMessageId: leaveProposalId ? null : cleanTransitionMessageId(value.successorTransitionMessageId),
10464
+ leaveProposalId
10443
10465
  };
10444
10466
  }
10445
10467
  function makeOwnerEpochRecord(manifest, epochSecret, fields = {}) {
@@ -10450,7 +10472,8 @@ function makeOwnerEpochRecord(manifest, epochSecret, fields = {}) {
10450
10472
  epochSecret: secretHex(epochSecret, "epoch secret"),
10451
10473
  cutoffMs: Number.isFinite(fields.cutoffMs) ? fields.cutoffMs : null,
10452
10474
  successorTransitionCommitment: fields.successorTransitionCommitment,
10453
- successorTransitionMessageId: fields.successorTransitionMessageId
10475
+ successorTransitionMessageId: fields.successorTransitionMessageId,
10476
+ leaveProposalId: fields.leaveProposalId
10454
10477
  });
10455
10478
  }
10456
10479
  async function sealOwnerEpochRecord(chatPrivateKey, entryId, epochEntryId, record) {
@@ -10602,6 +10625,32 @@ async function mutateOwnChatEntry(cloud, identity, entryId, current, update) {
10602
10625
  const committed = await cloud.user.chats.mutate(identity.uid, entryId, mutation);
10603
10626
  return committed?.result ?? mutation.result ?? null;
10604
10627
  }
10628
+ async function retireOwnChatEntry(cloud, identity, entryId, current, fields = {}) {
10629
+ const expectedEpochVersion = fields.expectedEpochVersion;
10630
+ const retirement = fields.retirement;
10631
+ if (!Number.isSafeInteger(expectedEpochVersion) || expectedEpochVersion < 1 || !retirement?.kind || !Number.isSafeInteger(retirement.epochVersion)) {
10632
+ throw new Error("chat owner retirement required");
10633
+ }
10634
+ return mutateOwnChatEntry(cloud, identity, entryId, current, (latest) => {
10635
+ if (latest?.retirement?.kind === retirement.kind && latest.retirement.epochVersion >= retirement.epochVersion) {
10636
+ return { result: latest };
10637
+ }
10638
+ if (latest?.retirement || latest?.current?.manifest?.epochVersion !== expectedEpochVersion) {
10639
+ throw new Error("chat owner epoch changed");
10640
+ }
10641
+ return {
10642
+ entry: {
10643
+ ...latest,
10644
+ routes: {},
10645
+ deliveryRegistered: false,
10646
+ notificationTag: null,
10647
+ retirement
10648
+ },
10649
+ epochs: fields.epoch ? [fields.epoch] : [],
10650
+ mlsDeletes: fields.mlsStateId ? [fields.mlsStateId] : []
10651
+ };
10652
+ });
10653
+ }
10605
10654
  async function openOwnChatMutationEntry(identity, entryId, record) {
10606
10655
  if (!record)
10607
10656
  return null;
@@ -11289,48 +11338,6 @@ function cleanFrontier(value) {
11289
11338
  throw new Error("chat receipt checkpoint frontier time required");
11290
11339
  return Object.freeze({ id, at: value.at, seenAt: value.seenAt });
11291
11340
  }
11292
- function maskBytes(memberCount2) {
11293
- return Math.ceil(memberCount2 / 8);
11294
- }
11295
- function cleanMask(value, memberCount2, label) {
11296
- const mask = cleanText(value).toLowerCase();
11297
- const size = maskBytes(memberCount2);
11298
- if (!new RegExp(`^[0-9a-f]{${size * 2}}$`, "u").test(mask)) {
11299
- throw new Error(`${label} invalid`);
11300
- }
11301
- const unused = size * 8 - memberCount2;
11302
- if (unused > 0 && Number.parseInt(mask.slice(-2), 16) & (1 << unused) - 1 << 8 - unused) {
11303
- throw new Error(`${label} overflow`);
11304
- }
11305
- return mask;
11306
- }
11307
- function makeMask(indices, memberCount2) {
11308
- const bytes = new Uint8Array(maskBytes(memberCount2));
11309
- for (const index of indices)
11310
- bytes[Math.floor(index / 8)] |= 1 << index % 8;
11311
- return [...bytes].map((value) => value.toString(16).padStart(2, "0")).join("");
11312
- }
11313
- function maskHas(mask, index) {
11314
- const offset = Math.floor(index / 8) * 2;
11315
- return (Number.parseInt(mask.slice(offset, offset + 2), 16) & 1 << index % 8) !== 0;
11316
- }
11317
- function maskCount(mask, memberCount2) {
11318
- let count = 0;
11319
- for (let index = 0;index < memberCount2; index += 1) {
11320
- if (maskHas(mask, index))
11321
- count += 1;
11322
- }
11323
- return count;
11324
- }
11325
- function frontierWithSeenAt(frontier, states) {
11326
- if (!frontier)
11327
- return null;
11328
- return {
11329
- id: frontier.id,
11330
- at: frontier.at,
11331
- seenAt: Math.max(frontier.seenAt, ...states.map((state) => Number(state?.readFrontier?.seenAt) || 0))
11332
- };
11333
- }
11334
11341
  function normalizeChatReceiptProjection(value) {
11335
11342
  if (!value || typeof value !== "object" || Array.isArray(value)) {
11336
11343
  throw new Error("chat receipt checkpoint projection required");
@@ -11339,41 +11346,13 @@ function normalizeChatReceiptProjection(value) {
11339
11346
  if (!Array.isArray(value.revisions) || value.revisions.length !== memberCount2) {
11340
11347
  throw new Error("chat receipt checkpoint revisions invalid");
11341
11348
  }
11342
- const lowFrontier = cleanFrontier(value.lowFrontier);
11343
- const nextLowFrontier = cleanFrontier(value.nextLowFrontier);
11344
- const highFrontier = cleanFrontier(value.highFrontier);
11345
- const nextHighFrontier = cleanFrontier(value.nextHighFrontier);
11346
- const lowMask = cleanMask(value.lowMask, memberCount2, "chat receipt checkpoint low mask");
11347
- const highMask = cleanMask(value.highMask, memberCount2, "chat receipt checkpoint high mask");
11348
- const nextHighMask = cleanMask(value.nextHighMask, memberCount2, "chat receipt checkpoint next high mask");
11349
- if (!highFrontier && maskCount(highMask, memberCount2) !== 0) {
11350
- throw new Error("chat receipt checkpoint high frontier missing");
11351
- }
11352
- if (highFrontier && maskCount(highMask, memberCount2) === 0) {
11353
- throw new Error("chat receipt checkpoint high readers missing");
11354
- }
11355
- if (!nextHighFrontier && maskCount(nextHighMask, memberCount2) !== 0) {
11356
- throw new Error("chat receipt checkpoint next high frontier missing");
11357
- }
11358
- if (nextHighFrontier && maskCount(nextHighMask, memberCount2) === 0) {
11359
- throw new Error("chat receipt checkpoint next high readers missing");
11360
- }
11361
- if (lowFrontier && compareReadFrontiers(lowFrontier, highFrontier) > 0) {
11362
- throw new Error("chat receipt checkpoint frontier order invalid");
11363
- }
11364
- if (nextLowFrontier && compareReadFrontiers(nextLowFrontier, highFrontier) > 0) {
11365
- throw new Error("chat receipt checkpoint next frontier order invalid");
11349
+ if (!Array.isArray(value.frontiers) || value.frontiers.length !== memberCount2) {
11350
+ throw new Error("chat receipt checkpoint frontiers invalid");
11366
11351
  }
11367
11352
  return Object.freeze({
11368
11353
  memberCount: memberCount2,
11369
11354
  revisions: Object.freeze(value.revisions.map(cleanRevision3)),
11370
- lowFrontier,
11371
- lowMask,
11372
- nextLowFrontier,
11373
- highFrontier,
11374
- highMask,
11375
- nextHighFrontier,
11376
- nextHighMask
11355
+ frontiers: Object.freeze(value.frontiers.map(cleanFrontier))
11377
11356
  });
11378
11357
  }
11379
11358
  function buildChatReceiptProjection(manifest, states) {
@@ -11381,36 +11360,66 @@ function buildChatReceiptProjection(manifest, states) {
11381
11360
  const memberCount2 = cleanCount(members.length);
11382
11361
  const values = members.map((member) => states?.get?.(member.chatPK) || null);
11383
11362
  const revisions = values.map((state) => Math.max(0, Number(state?.revision) || 0));
11384
- const missing = [];
11385
- const present = [];
11386
- values.forEach((state, index) => {
11387
- if (state?.readFrontier)
11388
- present.push({ index, state, frontier: state.readFrontier });
11389
- else
11390
- missing.push(index);
11391
- });
11392
- present.sort((left, right) => compareReadFrontiers(left.frontier, right.frontier));
11393
- const minimum = present[0]?.frontier || null;
11394
- const maximum = present.at(-1)?.frontier || null;
11395
- const lowIndices = missing.length ? missing : present.filter((item) => compareReadFrontiers(item.frontier, minimum) === 0).map((item) => item.index);
11396
- const nextLowItems = missing.length ? present : present.filter((item) => compareReadFrontiers(item.frontier, minimum) > 0);
11397
- const nextLow = nextLowItems[0]?.frontier || null;
11398
- const highItems = maximum ? present.filter((item) => compareReadFrontiers(item.frontier, maximum) === 0) : [];
11399
- const belowHigh = maximum ? present.filter((item) => compareReadFrontiers(item.frontier, maximum) < 0) : [];
11400
- const nextHigh = belowHigh.at(-1)?.frontier || null;
11401
- const nextHighItems = nextHigh ? belowHigh.filter((item) => compareReadFrontiers(item.frontier, nextHigh) === 0) : [];
11402
11363
  return normalizeChatReceiptProjection({
11403
11364
  memberCount: memberCount2,
11404
11365
  revisions,
11405
- lowFrontier: missing.length ? null : frontierWithSeenAt(minimum, values.filter(Boolean)),
11406
- lowMask: makeMask(lowIndices, memberCount2),
11407
- nextLowFrontier: frontierWithSeenAt(nextLow, nextLowItems.map((item) => item.state)),
11408
- highFrontier: frontierWithSeenAt(maximum, highItems.map((item) => item.state)),
11409
- highMask: makeMask(highItems.map((item) => item.index), memberCount2),
11410
- nextHighFrontier: frontierWithSeenAt(nextHigh, nextHighItems.map((item) => item.state)),
11411
- nextHighMask: makeMask(nextHighItems.map((item) => item.index), memberCount2)
11366
+ frontiers: values.map((state) => state?.readFrontier || null)
11412
11367
  });
11413
11368
  }
11369
+ function mergeChatReceiptProjections(current, incoming) {
11370
+ if (!current)
11371
+ return normalizeChatReceiptProjection(incoming);
11372
+ if (!incoming)
11373
+ return normalizeChatReceiptProjection(current);
11374
+ const left = normalizeChatReceiptProjection(current);
11375
+ const right = normalizeChatReceiptProjection(incoming);
11376
+ if (left.memberCount !== right.memberCount) {
11377
+ throw new Error("chat receipt checkpoint member count mismatch");
11378
+ }
11379
+ return normalizeChatReceiptProjection({
11380
+ memberCount: left.memberCount,
11381
+ revisions: left.revisions.map((revision, index) => Math.max(revision, right.revisions[index])),
11382
+ frontiers: left.frontiers.map((frontier, index) => {
11383
+ const other = right.frontiers[index];
11384
+ const comparison = compareReadFrontiers(frontier, other);
11385
+ if (comparison > 0)
11386
+ return frontier;
11387
+ if (comparison < 0)
11388
+ return other;
11389
+ if (!frontier)
11390
+ return null;
11391
+ return {
11392
+ ...frontier,
11393
+ seenAt: Math.max(frontier.seenAt, other.seenAt)
11394
+ };
11395
+ })
11396
+ });
11397
+ }
11398
+ function mergeChatReceiptCheckpointsByEpoch(current, incoming) {
11399
+ if (!(incoming instanceof Map) || !incoming.size) {
11400
+ return current instanceof Map ? current : new Map;
11401
+ }
11402
+ if (!(current instanceof Map) || !current.size)
11403
+ return new Map(incoming);
11404
+ const merged = new Map(current instanceof Map ? current : []);
11405
+ let changed = false;
11406
+ for (const [epochId, value] of incoming instanceof Map ? incoming : []) {
11407
+ const existing = merged.get(epochId);
11408
+ if (existing === value)
11409
+ continue;
11410
+ const exact = isChatReceiptCheckpoint(existing) && isChatReceiptCheckpoint(value) && existing.epochId === value.epochId && existing.memberChatPKs.length === value.memberChatPKs.length && existing.memberChatPKs.every((chatPK, index) => chatPK === value.memberChatPKs[index]);
11411
+ const next = exact ? Object.freeze({
11412
+ ...value,
11413
+ publishedAt: Math.max(Number(existing.publishedAt) || 0, Number(value.publishedAt) || 0) || null,
11414
+ projection: mergeChatReceiptProjections(existing.projection, value.projection)
11415
+ }) : value;
11416
+ if (exact && sameChatReceiptCheckpoint(existing, next) && existing.publishedAt === next.publishedAt)
11417
+ continue;
11418
+ merged.set(epochId, next);
11419
+ changed = true;
11420
+ }
11421
+ return changed ? merged : current;
11422
+ }
11414
11423
  function projectChatReceiptCheckpoint(manifest, value) {
11415
11424
  const projection = normalizeChatReceiptProjection(value?.projection || value);
11416
11425
  if (projection.memberCount !== manifest?.members?.length) {
@@ -11443,13 +11452,14 @@ function chatReceiptCheckpointCoversState(checkpoint, manifest, state) {
11443
11452
  function chatReceiptAllSeenFrontier(checkpoint, authorChatPK) {
11444
11453
  if (!isChatReceiptCheckpoint(checkpoint))
11445
11454
  return null;
11446
- const { lowFrontier, lowMask, nextLowFrontier, memberCount: memberCount2 } = checkpoint.projection;
11447
- const authorIndex = checkpoint.memberChatPKs?.indexOf?.(authorChatPK) ?? -1;
11448
- const lowCount = maskCount(lowMask, memberCount2);
11449
- if (authorIndex >= 0 && lowCount === 1 && maskHas(lowMask, authorIndex)) {
11450
- return nextLowFrontier;
11451
- }
11452
- return lowFrontier;
11455
+ const readers = checkpoint.projection.frontiers.filter((_frontier, index) => checkpoint.memberChatPKs[index] !== authorChatPK);
11456
+ if (!readers.length || readers.some((frontier) => !frontier))
11457
+ return null;
11458
+ const minimum = readers.reduce((current, frontier) => !current || compareReadFrontiers(frontier, current) < 0 ? frontier : current, null);
11459
+ return Object.freeze({
11460
+ ...minimum,
11461
+ seenAt: Math.max(...readers.map((frontier) => frontier.seenAt))
11462
+ });
11453
11463
  }
11454
11464
  function chatReceiptProjectionFingerprint(value) {
11455
11465
  return JSON.stringify(normalizeChatReceiptProjection(value));
@@ -13563,10 +13573,6 @@ async function prepareOwnerChatMlsSnapshot(identity, entryId, epochState, snapsh
13563
13573
  function ownerChatMlsSnapshotId(identity, entryId, epochState) {
13564
13574
  return stateAddress(identity, entryId, epochState).stateId;
13565
13575
  }
13566
- async function discardOwnerChatMlsSnapshot(cloud, identity, entryId, epochState) {
13567
- const address = stateAddress(identity, entryId, epochState);
13568
- await cloud.user.chats.mls.delete(identity.uid, entryId, address.stateId).catch(() => false);
13569
- }
13570
13576
 
13571
13577
  // ../../core/chat/epochs/membership.js
13572
13578
  "use client";
@@ -15162,7 +15168,45 @@ function reviveMarker(marker) {
15162
15168
  function serializeKeys(keys) {
15163
15169
  return [...keySet(keys)].filter(Boolean).sort();
15164
15170
  }
15165
- function serializeBatch(batch, { expiredKeys, deletedKeys } = {}) {
15171
+ function cleanReceiptCheckpoint(value) {
15172
+ if (!isChatReceiptCheckpoint(value))
15173
+ return null;
15174
+ const epochId = cleanText(value.epochId);
15175
+ const memberChatPKs = (value.memberChatPKs || []).map(cleanText).filter(Boolean);
15176
+ if (!epochId || memberChatPKs.length !== value.memberChatPKs?.length)
15177
+ return null;
15178
+ try {
15179
+ return projectChatReceiptCheckpoint({
15180
+ epochId,
15181
+ members: memberChatPKs.map((chatPK) => ({ chatPK }))
15182
+ }, value);
15183
+ } catch {
15184
+ return null;
15185
+ }
15186
+ }
15187
+ function serializeReceiptCheckpoints(statesByEpoch, epochIds) {
15188
+ const allowed = new Set(epochIds || []);
15189
+ const checkpoints = [];
15190
+ for (const [epochId, value] of statesByEpoch instanceof Map ? statesByEpoch : []) {
15191
+ if (allowed.size && !allowed.has(epochId))
15192
+ continue;
15193
+ const checkpoint = cleanReceiptCheckpoint(value);
15194
+ if (checkpoint)
15195
+ checkpoints.push(checkpoint);
15196
+ }
15197
+ return checkpoints.sort((left, right) => left.epochId.localeCompare(right.epochId));
15198
+ }
15199
+ function reviveReceiptCheckpoints(values) {
15200
+ return new Map((Array.isArray(values) ? values : []).map((value) => cleanReceiptCheckpoint(value)).filter(Boolean).map((checkpoint) => [checkpoint.epochId, checkpoint]));
15201
+ }
15202
+ function receiptEpochIds(messages, fallbackEpochId = "") {
15203
+ const ids = new Set((messages || []).map((message) => cleanText(message?.epochId)).filter(Boolean));
15204
+ const fallback = cleanText(fallbackEpochId);
15205
+ if (fallback)
15206
+ ids.add(fallback);
15207
+ return ids;
15208
+ }
15209
+ function serializeBatch(batch, { expiredKeys, deletedKeys, epochIds } = {}) {
15166
15210
  if (!batch && !expiredKeys?.size && !deletedKeys?.size) {
15167
15211
  return null;
15168
15212
  }
@@ -15172,14 +15216,16 @@ function serializeBatch(batch, { expiredKeys, deletedKeys } = {}) {
15172
15216
  ...Number.isSafeInteger(batch?.epochVersion) ? { epochVersion: batch.epochVersion } : {},
15173
15217
  ...Number.isFinite(batch?.firstMs) ? { firstMs: batch.firstMs } : {},
15174
15218
  ...Number.isFinite(batch?.lastMs) ? { lastMs: batch.lastMs } : {},
15219
+ receiptCheckpoints: serializeReceiptCheckpoints(batch?.memberStatesByEpoch, epochIds),
15175
15220
  expiredKeys: serializeKeys(expiredKeys || batch?.expiredKeys),
15176
15221
  deletedKeys: serializeKeys(deletedKeys || batch?.deletedKeys)
15177
15222
  };
15178
15223
  }
15179
- function batchFromLive(latestMessages, expiredKeys, deletedKeys, fallback) {
15224
+ function batchFromLive(latestMessages, expiredKeys, deletedKeys, fallback, receiptMessages = latestMessages) {
15180
15225
  const source = latestMessages || [];
15226
+ const epochIds = receiptEpochIds(receiptMessages, fallback?.epochId);
15181
15227
  if (!source.length) {
15182
- return serializeBatch(fallback, { expiredKeys, deletedKeys });
15228
+ return serializeBatch(fallback, { expiredKeys, deletedKeys, epochIds });
15183
15229
  }
15184
15230
  const firstMs = getMessageOrderMs(source[0]);
15185
15231
  const lastMs = getMessageOrderMs(source[source.length - 1]);
@@ -15187,8 +15233,9 @@ function batchFromLive(latestMessages, expiredKeys, deletedKeys, fallback) {
15187
15233
  firstMs,
15188
15234
  lastMs,
15189
15235
  epochId: fallback?.epochId,
15190
- epochVersion: fallback?.epochVersion
15191
- } : fallback, { expiredKeys, deletedKeys });
15236
+ epochVersion: fallback?.epochVersion,
15237
+ memberStatesByEpoch: fallback?.memberStatesByEpoch
15238
+ } : fallback, { expiredKeys, deletedKeys, epochIds });
15192
15239
  }
15193
15240
  function reviveBatch(batch, expiredKeys, deletedKeys) {
15194
15241
  const hasRange = Number.isFinite(batch?.firstMs) && Number.isFinite(batch?.lastMs);
@@ -15200,6 +15247,7 @@ function reviveBatch(batch, expiredKeys, deletedKeys) {
15200
15247
  ...cleanText(batch?.epochId) ? { epochId: cleanText(batch.epochId) } : {},
15201
15248
  ...Number.isSafeInteger(batch?.epochVersion) ? { epochVersion: batch.epochVersion } : {},
15202
15249
  ...hasRange ? { firstMs: batch.firstMs, lastMs: batch.lastMs } : {},
15250
+ memberStatesByEpoch: reviveReceiptCheckpoints(batch?.receiptCheckpoints),
15203
15251
  expiredKeys,
15204
15252
  deletedKeys
15205
15253
  };
@@ -15216,6 +15264,7 @@ function mergeBatches(left, right) {
15216
15264
  ...Number.isSafeInteger(right?.epochVersion ?? left?.epochVersion) ? { epochVersion: right?.epochVersion ?? left?.epochVersion } : {},
15217
15265
  ...firstValues.length ? { firstMs: Math.min(...firstValues) } : {},
15218
15266
  ...lastValues.length ? { lastMs: Math.max(...lastValues) } : {},
15267
+ memberStatesByEpoch: mergeChatReceiptCheckpointsByEpoch(left?.memberStatesByEpoch, right?.memberStatesByEpoch),
15219
15268
  expiredKeys: keySet([...left?.expiredKeys || [], ...right?.expiredKeys || []]),
15220
15269
  deletedKeys: keySet([...left?.deletedKeys || [], ...right?.deletedKeys || []])
15221
15270
  };
@@ -15558,7 +15607,7 @@ function writeCachedMessageState(cache, { stateVersion = null, chatId, selfChatP
15558
15607
  historyStartReached: nextHistoryStartReached === true,
15559
15608
  olderThan: serializeMarker(storedOlderThan),
15560
15609
  olderLoaded: nextOlderLoaded === true || compacted.historyMessages.length > 0,
15561
- latestServerPage: batchFromLive(compacted.latestMessages, expiredKeys, deletedKeys, nextServerBatch),
15610
+ latestServerPage: batchFromLive(compacted.latestMessages, expiredKeys, deletedKeys, nextServerBatch, compacted.all),
15562
15611
  deletedKeys: serializeKeys(deletedKeys),
15563
15612
  expiredKeys: serializeKeys(expiredKeys),
15564
15613
  historyMessages: compacted.historyMessages,
@@ -17778,10 +17827,16 @@ function createChatLive({
17778
17827
  onActivity?.(entry.chatId, EMPTY_ACTIVITY);
17779
17828
  };
17780
17829
  const enterChatActivity = async (chatId) => {
17781
- if (!enabled || !chatId || rooms.has(chatId))
17830
+ if (!enabled || !chatId)
17782
17831
  return false;
17832
+ const current = rooms.get(chatId);
17833
+ if (current && !current.closed) {
17834
+ current.retainers += 1;
17835
+ return true;
17836
+ }
17783
17837
  const entry = {
17784
17838
  chatId,
17839
+ retainers: 1,
17785
17840
  epochId: "",
17786
17841
  material: null,
17787
17842
  connection: null,
@@ -17857,6 +17912,9 @@ function createChatLive({
17857
17912
  const entry = rooms.get(chatId);
17858
17913
  if (!entry)
17859
17914
  return false;
17915
+ entry.retainers -= 1;
17916
+ if (entry.retainers > 0)
17917
+ return true;
17860
17918
  rooms.delete(chatId);
17861
17919
  destroy(entry);
17862
17920
  return true;
@@ -17904,13 +17962,26 @@ function createChatLive({
17904
17962
  publish(entry);
17905
17963
  return scheduleReadSnapshot(entry);
17906
17964
  };
17965
+ const flushChatReadFrontier = async (chatId) => {
17966
+ const entry = rooms.get(chatId);
17967
+ if (!entry || entry.closed || !entry.connected || !entry.readFrontier)
17968
+ return false;
17969
+ if (!sendSnapshot(entry))
17970
+ return false;
17971
+ const pending = entry.sendChain;
17972
+ await pending;
17973
+ return !entry.closed;
17974
+ };
17907
17975
  const leaveAllChatActivity = () => {
17908
- for (const chatId of [...rooms.keys()])
17909
- leaveChatActivity(chatId);
17976
+ for (const [chatId, entry] of [...rooms]) {
17977
+ rooms.delete(chatId);
17978
+ destroy(entry);
17979
+ }
17910
17980
  };
17911
17981
  return Object.freeze({
17912
17982
  close: leaveAllChatActivity,
17913
17983
  enterChatActivity,
17984
+ flushChatReadFrontier,
17914
17985
  leaveChatActivity,
17915
17986
  leaveAllChatActivity,
17916
17987
  markChatReadFrontier,
@@ -21311,12 +21382,12 @@ function chatConversationStateKey({ chatId, selfChatPublicKey, unlocked, startMs
21311
21382
 
21312
21383
  // ../../core/crypto/chatreceipt.js
21313
21384
  "use client";
21314
- var CHAT_RECEIPT_CHECKPOINT_VERSION = 1;
21315
- var CHAT_RECEIPT_CHECKPOINT_MAX_BYTES = 4096;
21385
+ var CHAT_RECEIPT_CHECKPOINT_VERSION = 2;
21386
+ var CHAT_RECEIPT_CHECKPOINT_MAX_BYTES = 16 * 1024;
21316
21387
  var CHECKPOINT_BODY_VERSION = 1;
21317
21388
  var CHECKPOINT_BODY_SUITE = 1;
21318
21389
  var CHECKPOINT_HEADER_BYTES = 2 + BOX_NONCE_BYTES;
21319
- var CHECKPOINT_SIGN_SCOPE = encoder.encode("veyl-chat-receipt-checkpoint-v3-sign");
21390
+ var CHECKPOINT_SIGN_SCOPE = encoder.encode("veyl-chat-receipt-checkpoint-v4-sign");
21320
21391
  function cleanPositiveInteger(value, label) {
21321
21392
  if (!Number.isSafeInteger(value) || value <= 0)
21322
21393
  throw new Error(`${label} required`);
@@ -21529,6 +21600,7 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
21529
21600
  let closed = false;
21530
21601
  let closing = false;
21531
21602
  let checkpoint = null;
21603
+ let displayedProjection = null;
21532
21604
  let sourceProjection = null;
21533
21605
  let sourceStop = null;
21534
21606
  let sourceLeaseTimer = null;
@@ -21594,8 +21666,10 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
21594
21666
  checkpoint = next;
21595
21667
  if (closing || closed)
21596
21668
  return;
21597
- if (next)
21598
- onUpdate?.(projected(next));
21669
+ if (next) {
21670
+ displayedProjection = mergeChatReceiptProjections(displayedProjection, next.projection);
21671
+ onUpdate?.(projected({ ...next, projection: displayedProjection }));
21672
+ }
21599
21673
  if (coversDesired(next)) {
21600
21674
  desiredState = null;
21601
21675
  clearStalledClaimTimer();
@@ -21608,10 +21682,10 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
21608
21682
  else
21609
21683
  armSourceExpiry();
21610
21684
  };
21611
- const writeCheckpoint = async (mode, expectedVersion = null) => {
21612
- if (closed || closing)
21685
+ const writeCheckpoint = async (mode, expectedVersion = null, projectionOverride = null) => {
21686
+ if (closed || closing && mode !== "release")
21613
21687
  return false;
21614
- const desiredProjection = mode === "publish" ? sourceProjection : null;
21688
+ const desiredProjection = mode === "publish" || mode === "release" ? projectionOverride || sourceProjection : null;
21615
21689
  const result = await cloud.chat.memberState.write({
21616
21690
  epochId: epoch.epochId,
21617
21691
  lane: address.lane,
@@ -21622,19 +21696,22 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
21622
21696
  const current = record ? await openChatReceiptCheckpoint(epoch, record).catch(() => null) : null;
21623
21697
  const heldByOther = !holdsLease(current, now) && Number(current?.leaseUntil) > now;
21624
21698
  const stalledTakeover = mode === "claim" && expectedVersion && checkpointVersion(current) === expectedVersion && !coversDesired(current);
21699
+ if (mode === "release" && !holdsLease(current, now)) {
21700
+ return { result: { won: false, checkpoint: current } };
21701
+ }
21625
21702
  if (heldByOther && !stalledTakeover)
21626
21703
  return { result: { won: false, checkpoint: current } };
21627
- const projection = desiredProjection || current?.projection || buildChatReceiptProjection(epoch.manifest, new Map);
21704
+ const projection = desiredProjection ? mergeChatReceiptProjections(current?.projection, desiredProjection) : current?.projection || buildChatReceiptProjection(epoch.manifest, new Map);
21628
21705
  if (mode === "publish" && holdsLease(current, now) && chatReceiptProjectionFingerprint(current.projection) === chatReceiptProjectionFingerprint(projection)) {
21629
21706
  return { result: { won: true, checkpoint: current } };
21630
21707
  }
21631
21708
  const term = current ? current.term + (!holdsLease(current, now) || Number(current.leaseUntil) <= now ? 1 : 0) : 1;
21632
21709
  const sealed = await sealChatReceiptCheckpoint(epoch, {
21633
- v: 1,
21710
+ v: CHAT_RECEIPT_CHECKPOINT_VERSION,
21634
21711
  coordinator: epoch.actor.chatPK,
21635
21712
  holder,
21636
21713
  term,
21637
- leaseUntil: now + RECEIPT_COORDINATOR_LEASE_MS,
21714
+ leaseUntil: mode === "release" ? now : now + RECEIPT_COORDINATOR_LEASE_MS,
21638
21715
  publishedAt: now,
21639
21716
  projection
21640
21717
  });
@@ -21684,10 +21761,10 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
21684
21761
  sourceStop = watchChatMemberStateEvidence(cloud, epochState, ({ states }) => {
21685
21762
  if (closed || closing || !holdsLease(checkpoint))
21686
21763
  return;
21687
- const nextProjection = buildChatReceiptProjection(epoch.manifest, states);
21688
- sourceProjection = nextProjection;
21764
+ sourceProjection = mergeChatReceiptProjections(checkpoint?.projection, buildChatReceiptProjection(epoch.manifest, states));
21765
+ displayedProjection = mergeChatReceiptProjections(displayedProjection, sourceProjection);
21689
21766
  onUpdate?.(projectChatReceiptCheckpoint(epoch.manifest, {
21690
- projection: sourceProjection,
21767
+ projection: displayedProjection,
21691
21768
  publishedAt: Date.now()
21692
21769
  }));
21693
21770
  schedulePublish();
@@ -21777,6 +21854,8 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
21777
21854
  if (closed || closing)
21778
21855
  return;
21779
21856
  closing = true;
21857
+ const finalProjection = sourceProjection;
21858
+ const release = holdsLease(checkpoint) ? track(writeCheckpoint("release", null, finalProjection)) : null;
21780
21859
  watchRun += 1;
21781
21860
  clearClaimTimer();
21782
21861
  clearStalledClaimTimer();
@@ -21790,7 +21869,7 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
21790
21869
  closing = false;
21791
21870
  closeReceiptEpoch(opened);
21792
21871
  };
21793
- Promise.allSettled([...operations]).finally(finish);
21872
+ Promise.allSettled([...operations, release].filter(Boolean)).finally(finish);
21794
21873
  }
21795
21874
  };
21796
21875
  }
@@ -22835,6 +22914,55 @@ function createChatMessageBatches({ cloud, media = {}, chatPK, chatSigningPK, ch
22835
22914
  };
22836
22915
  }
22837
22916
 
22917
+ // ../../core/chat/retirement.js
22918
+ var CHAT_RETIREMENT_ACTIONS = Object.freeze({
22919
+ DELETE: "delete",
22920
+ LEAVE: "leave"
22921
+ });
22922
+ function planChatRetirement(chat, selfChatPK) {
22923
+ const self = cleanText(selfChatPK);
22924
+ const members = Array.isArray(chat?.members) ? chat.members : [];
22925
+ const remaining = members.filter((member) => cleanText(member?.chatPK) !== self);
22926
+ const direct = chat?.lineage === CHAT_LINEAGES.DIRECT && members.length === 2;
22927
+ return {
22928
+ action: direct || !remaining.length ? CHAT_RETIREMENT_ACTIONS.DELETE : CHAT_RETIREMENT_ACTIONS.LEAVE,
22929
+ remaining
22930
+ };
22931
+ }
22932
+ async function retireChatMembershipOwner(cloud, identity, entry, fields = {}) {
22933
+ const parent = entry?.current?.manifest;
22934
+ const kind = fields.kind;
22935
+ const epochVersion = fields.epochVersion;
22936
+ if (!parent?.chatId || !identity?.uid || !identity?.chatPrivateKey || !Object.values(CHAT_OWNER_RETIREMENT_KINDS).includes(kind) || !Number.isSafeInteger(epochVersion)) {
22937
+ throw new Error("chat membership retirement required");
22938
+ }
22939
+ const entryId = ownChatEntryId(identity.chatPrivateKey, parent.chatId);
22940
+ const historyId = ownEpochEntryId(identity.chatPrivateKey, parent.chatId, parent.epochId);
22941
+ const cutoffMs = Number.isFinite(fields.cutoffMs) ? fields.cutoffMs : Date.now();
22942
+ const history = makeOwnerEpochRecord(parent, entry.current.epochSecret, {
22943
+ cutoffMs,
22944
+ successorTransitionCommitment: fields.successorTransitionCommitment,
22945
+ successorTransitionMessageId: fields.successorTransitionMessageId,
22946
+ leaveProposalId: fields.leaveProposalId
22947
+ });
22948
+ return retireOwnChatEntry(cloud, identity, entryId, entry, {
22949
+ expectedEpochVersion: parent.epochVersion,
22950
+ retirement: {
22951
+ kind,
22952
+ epochVersion,
22953
+ ...kind === CHAT_OWNER_RETIREMENT_KINDS.LEAVE ? { proposalId: fields.leaveProposalId } : {}
22954
+ },
22955
+ epoch: {
22956
+ id: historyId,
22957
+ record: {
22958
+ body: await sealOwnerEpochRecord(identity.chatPrivateKey, entryId, historyId, history),
22959
+ tsMs: cutoffMs
22960
+ }
22961
+ },
22962
+ mlsStateId: ownerChatMlsSnapshotId(identity, entryId, entry.current)
22963
+ });
22964
+ }
22965
+
22838
22966
  // ../../core/chat/inbox.js
22839
22967
  "use client";
22840
22968
  function normalizeChatPreview(record, message) {
@@ -23138,7 +23266,7 @@ function sameRoutes(left, right) {
23138
23266
  return leftKeys.length === rightKeys.length && leftKeys.every((key) => sameRoute(left[key], right[key]));
23139
23267
  }
23140
23268
  function sameStableEntry(left, right) {
23141
- return !!left && !!right && left.current?.manifest?.epochId === right.current?.manifest?.epochId && left.current?.settings?.digest === right.current?.settings?.digest && sameRoutes(left.routes, right.routes) && left.deliveryRegistered === right.deliveryRegistered && left.notificationTag === right.notificationTag;
23269
+ return !!left && !!right && left.current?.manifest?.epochId === right.current?.manifest?.epochId && left.current?.settings?.digest === right.current?.settings?.digest && sameRoutes(left.routes, right.routes) && left.deliveryRegistered === right.deliveryRegistered && left.notificationTag === right.notificationTag && left.retirement?.kind === right.retirement?.kind && left.retirement?.epochVersion === right.retirement?.epochVersion;
23142
23270
  }
23143
23271
  function projectOwnChatEntry(entry, entryId, userChatPK, ts, activity = {}) {
23144
23272
  const manifest = entry.current.manifest;
@@ -23188,13 +23316,17 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
23188
23316
  }
23189
23317
  let knownMember = entry?.current?.manifest?.members?.find((member) => member.chatPK === payload.senderChatPK) || null;
23190
23318
  const senderProfile = openedProfile || (knownMember ? { ...knownMember, uid: knownMember.uid } : null) || await resolveSenderProfile(cloud, payload);
23191
- if (entry && payload.epochVersion === entry.current.manifest.epochVersion && cleanText(payload.messageId) && cleanText(currentChat?.inboxMessageId) === cleanText(payload.messageId)) {
23319
+ let restoringMembership = !!entry?.retirement;
23320
+ if (restoringMembership && (payload.kind !== "welcome" || payload.epochVersion <= entry.retirement.epochVersion)) {
23321
+ return "stale";
23322
+ }
23323
+ if (entry && !restoringMembership && payload.epochVersion === entry.current.manifest.epochVersion && cleanText(payload.messageId) && cleanText(currentChat?.inboxMessageId) === cleanText(payload.messageId)) {
23192
23324
  return "duplicate";
23193
23325
  }
23194
- if (entry && payload.epochVersion < entry.current.manifest.epochVersion) {
23326
+ if (entry && !restoringMembership && payload.epochVersion < entry.current.manifest.epochVersion) {
23195
23327
  return "stale";
23196
23328
  }
23197
- if (entry && payload.epochVersion > entry.current.manifest.epochVersion) {
23329
+ if (entry && !restoringMembership && payload.epochVersion > entry.current.manifest.epochVersion) {
23198
23330
  let advanced;
23199
23331
  try {
23200
23332
  advanced = await advanceEntryToEpoch(cloud, uid, identity, entry, payload.epochVersion, options);
@@ -23211,11 +23343,16 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
23211
23343
  }
23212
23344
  }
23213
23345
  if (advanced?.removed) {
23214
- options.onPingDelete?.(payload.chatId);
23215
- return "deleted";
23346
+ if (payload.kind !== "welcome" || payload.epochVersion <= advanced.entry?.retirement?.epochVersion) {
23347
+ options.onPingDelete?.(payload.chatId);
23348
+ return "deleted";
23349
+ }
23350
+ entry = advanced.entry;
23351
+ restoringMembership = true;
23352
+ } else {
23353
+ entry = advanced;
23354
+ knownMember = entry.current.manifest.members.find((member) => member.chatPK === payload.senderChatPK) || null;
23216
23355
  }
23217
- entry = advanced;
23218
- knownMember = entry.current.manifest.members.find((member) => member.chatPK === payload.senderChatPK) || null;
23219
23356
  }
23220
23357
  if (payload.kind === "chat_deleted") {
23221
23358
  if (!entry || entry.current.manifest.epochId !== payload.epochId || entry.current.manifest.epochVersion !== payload.epochVersion || !knownMember) {
@@ -23235,7 +23372,7 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
23235
23372
  return "deleted";
23236
23373
  }
23237
23374
  let epochState = entry?.current || null;
23238
- if (!entry && payload.kind === "welcome") {
23375
+ if ((!entry || restoringMembership) && payload.kind === "welcome") {
23239
23376
  epochState = await welcomedEpochFromPing(cloud, identity, senderProfile, payload, options);
23240
23377
  } else if (!entry) {
23241
23378
  throw new Error("chat mls welcome required");
@@ -23283,7 +23420,7 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
23283
23420
  const routes = withSenderRoute(entry, payload, senderProfile, epochState);
23284
23421
  const preview = nextPreview(currentChat, record, message, payload);
23285
23422
  const nowMs2 = timestampMs(record?.ts, null) ?? timestampMs(payload.ts, Date.now()) ?? Date.now();
23286
- const membershipWelcome = !entry && payload.kind === "welcome";
23423
+ const membershipWelcome = (!entry || restoringMembership) && payload.kind === "welcome";
23287
23424
  const projectedTsMs = membershipWelcome ? Date.now() : nowMs2;
23288
23425
  let deliveryRegistered = entry?.deliveryRegistered === true;
23289
23426
  if (!deliveryRegistered)
@@ -23301,7 +23438,8 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
23301
23438
  },
23302
23439
  routes,
23303
23440
  deliveryRegistered,
23304
- notificationTag: notificationChatTag(epochState.stateCapability, payload.chatId, identity.chatPK)
23441
+ notificationTag: notificationChatTag(epochState.stateCapability, payload.chatId, identity.chatPK),
23442
+ retirement: null
23305
23443
  } : makeOwnChatEntry(epochState, {
23306
23444
  routes,
23307
23445
  startMs: nowMs2,
@@ -23317,6 +23455,9 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
23317
23455
  ...welcomeMlsState ? { mlsWrites: [{ id: welcomeMlsState.stateId, value: welcomeMlsState.value }] } : {}
23318
23456
  };
23319
23457
  }
23458
+ if (membershipWelcome && current.retirement && epochState.manifest.epochVersion <= current.retirement.epochVersion) {
23459
+ throw new Error("chat welcome is not newer than retirement");
23460
+ }
23320
23461
  if (current.current.manifest.epochVersion > epochState.manifest.epochVersion) {
23321
23462
  return { result: current };
23322
23463
  }
@@ -23325,7 +23466,8 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
23325
23466
  current: nextEntry.current,
23326
23467
  routes: withSenderRoute(current, payload, senderProfile, epochState),
23327
23468
  deliveryRegistered: current.deliveryRegistered || nextEntry.deliveryRegistered,
23328
- notificationTag: nextEntry.notificationTag
23469
+ notificationTag: nextEntry.notificationTag,
23470
+ retirement: membershipWelcome ? null : current.retirement
23329
23471
  };
23330
23472
  if (sameStableEntry(current, candidate) && !welcomeMlsState)
23331
23473
  return { result: current };
@@ -23335,6 +23477,9 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
23335
23477
  ...welcomeMlsState ? { mlsWrites: [{ id: welcomeMlsState.stateId, value: welcomeMlsState.value }] } : {}
23336
23478
  };
23337
23479
  }) : entry;
23480
+ if (committedEntry?.retirement) {
23481
+ throw new Error("chat membership remains retired");
23482
+ }
23338
23483
  if (membershipWelcome && epochState.mlsSnapshot) {
23339
23484
  if (!epochState.mlsKeyPackageLastResort) {
23340
23485
  await cloud.user.mls.keyPackages.deleteState(identity.uid, epochState.mlsKeyPackageId).catch(() => false);
@@ -23439,15 +23584,19 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
23439
23584
  });
23440
23585
  await cloud.delivery.revoke(capability).catch(() => false);
23441
23586
  }
23442
- if (options.wasRemoved?.(parent.chatId) !== true) {
23443
- markDiag(options.diag, "chat.owner.unlink", { reason: "membership" });
23444
- await cloud.user.chats.unlink(uid, entryId);
23445
- }
23587
+ markDiag(options.diag, "chat.owner.retire", { reason: "membership" });
23588
+ const retiredEntry = await retireChatMembershipOwner(cloud, identity, entry, {
23589
+ kind: CHAT_OWNER_RETIREMENT_KINDS.REMOVED,
23590
+ epochVersion: confirmed.nextEpochVersion,
23591
+ cutoffMs: Date.now(),
23592
+ successorTransitionCommitment: confirmed.transitionCommitment,
23593
+ successorTransitionMessageId: messageId
23594
+ });
23446
23595
  if (leaveProposalId) {
23447
23596
  options.leaveProposalObservedAt?.delete(`${parent.chatId}:${leaveProposalId}`);
23448
23597
  }
23449
23598
  options.onRemoved?.(parent.chatId, confirmed.nextEpochVersion);
23450
- return { removed: true, chatId: parent.chatId };
23599
+ return { removed: true, chatId: parent.chatId, entry: retiredEntry };
23451
23600
  }
23452
23601
  openStage = "mls-package-open";
23453
23602
  const opened = await openChatMlsEpochPackage(processed, packageRecord, {
@@ -24110,6 +24259,9 @@ async function decryptChatEntry(entryRecord, userChatPK, userPrivKey) {
24110
24259
  if (entry.ownerRevision !== data?.revision) {
24111
24260
  throw new Error("chat owner revision mismatch");
24112
24261
  }
24262
+ if (entry.retirement) {
24263
+ return null;
24264
+ }
24113
24265
  if (entry.protocol !== CHAT_PROTOCOL_VERSION) {
24114
24266
  return null;
24115
24267
  }
@@ -25188,22 +25340,6 @@ function createChatList({
25188
25340
  };
25189
25341
  }
25190
25342
 
25191
- // ../../core/chat/retirement.js
25192
- var CHAT_RETIREMENT_ACTIONS = Object.freeze({
25193
- DELETE: "delete",
25194
- LEAVE: "leave"
25195
- });
25196
- function planChatRetirement(chat, selfChatPK) {
25197
- const self = cleanText(selfChatPK);
25198
- const members = Array.isArray(chat?.members) ? chat.members : [];
25199
- const remaining = members.filter((member) => cleanText(member?.chatPK) !== self);
25200
- const direct = chat?.lineage === CHAT_LINEAGES.DIRECT && members.length === 2;
25201
- return {
25202
- action: direct || !remaining.length ? CHAT_RETIREMENT_ACTIONS.DELETE : CHAT_RETIREMENT_ACTIONS.LEAVE,
25203
- remaining
25204
- };
25205
- }
25206
-
25207
25343
  // ../../core/utils/appstate.js
25208
25344
  function isForegroundAppState(state) {
25209
25345
  const value = String(state || "").trim().toLowerCase();
@@ -25431,6 +25567,7 @@ function createChatSession({
25431
25567
  deleteAllChats,
25432
25568
  restoreDeletedChat,
25433
25569
  enterChatActivity,
25570
+ flushChatReadFrontier,
25434
25571
  leaveChatActivity,
25435
25572
  markChatReadState,
25436
25573
  markChatRead,
@@ -26357,9 +26494,13 @@ function createChatSession({
26357
26494
  recipientChatPK: chatPK,
26358
26495
  generation: chat.epochVersion
26359
26496
  });
26497
+ await retireChatMembershipOwner(cloud, transitionIdentity(), chat.ownEntry, {
26498
+ kind: CHAT_OWNER_RETIREMENT_KINDS.LEAVE,
26499
+ epochVersion: chat.epochVersion,
26500
+ cutoffMs: Date.now(),
26501
+ leaveProposalId: proposal.message.cid
26502
+ });
26360
26503
  await cloud.delivery.revoke(capability).catch(() => false);
26361
- await discardOwnerChatMlsSnapshot(cloud, transitionIdentity(), chat.entryId, chat.epochState);
26362
- await cloud.user.chats.unlink(uid, chat.entryId);
26363
26504
  if (retirementStarted) {
26364
26505
  deleteActions.commitRemovedRetirement(chatId, chat.epochVersion + 1);
26365
26506
  retirementStarted = false;
@@ -26716,11 +26857,12 @@ function createChatSession({
26716
26857
  const dropChat = (...args) => deleteActions.dropChat(...args);
26717
26858
  const dropUnavailableChat = (...args) => deleteActions.dropUnavailableChat(...args);
26718
26859
  const deleteChat = (...args) => deleteActions.deleteChat(...args);
26719
- const retireChats = async (affected) => {
26860
+ const purgePeerChats = async (affected) => {
26720
26861
  for (const chat of affected) {
26721
26862
  const retirement = planChatRetirement(chat, chatPK);
26722
26863
  if (retirement.action === CHAT_RETIREMENT_ACTIONS.LEAVE) {
26723
26864
  await leaveOwnedChat(chat, { optimistic: false });
26865
+ await cloud.user.chats.unlink(uid, chat.entryId);
26724
26866
  } else {
26725
26867
  await deleteChat(chat, { cleanup: false });
26726
26868
  }
@@ -26730,7 +26872,7 @@ function createChatSession({
26730
26872
  const retirePeerChat = async (peerChatPK, options = {}) => {
26731
26873
  const allChats = await loadAllChats(cloud, uid, chatPK, chatPrivateKey);
26732
26874
  const affected = allChats.filter((chat) => chat?.members?.some((member) => member.chatPK === peerChatPK));
26733
- await retireChats(affected);
26875
+ await purgePeerChats(affected);
26734
26876
  await options.onDeliveryRevoked?.();
26735
26877
  return affected.length > 0;
26736
26878
  };
@@ -26739,7 +26881,7 @@ function createChatSession({
26739
26881
  return 0;
26740
26882
  const allChats = await loadAllChats(cloud, uid, chatPK, chatPrivateKey);
26741
26883
  const affected = allChats.filter((chat) => chat?.members?.some((member) => member.chatPK !== chatPK && blockedUids.has(member.uid)));
26742
- return retireChats(affected);
26884
+ return purgePeerChats(affected);
26743
26885
  };
26744
26886
  const restoreDeletedChat = (...args) => deleteActions.restoreDeletedChat(...args);
26745
26887
  const wasChatDeletedLocally = (...args) => deleteActions.wasChatDeletedLocally(...args);
@@ -26753,6 +26895,7 @@ function createChatSession({
26753
26895
  });
26754
26896
  return liveActions.enterChatActivity(chatId);
26755
26897
  };
26898
+ const flushChatReadFrontier = (chatId) => liveActions.flushChatReadFrontier(chatId);
26756
26899
  const leaveChatActivity = (chatId) => {
26757
26900
  const flush = seenActions.flushChatRead(chatId);
26758
26901
  liveActions.leaveChatActivity(chatId);