@glyphteck/veyl 0.60.0 → 0.61.1
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 +319 -176
- package/dist/auth.js +2 -1
- package/dist/cli.js +487 -201
- package/dist/index.js +487 -201
- package/docs/agents.md +1 -1
- package/docs/api.md +1 -1
- package/examples/bot-fleet/policy.js +136 -17
- package/examples/bot-fleet/readme.md +5 -3
- package/examples/bot-fleet/runtime.js +6 -2
- package/package.json +1 -1
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
|
-
|
|
11343
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
11447
|
-
|
|
11448
|
-
|
|
11449
|
-
|
|
11450
|
-
|
|
11451
|
-
|
|
11452
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
17909
|
-
|
|
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 =
|
|
21315
|
-
var CHAT_RECEIPT_CHECKPOINT_MAX_BYTES =
|
|
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-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
21688
|
-
|
|
21764
|
+
sourceProjection = mergeChatReceiptProjections(checkpoint?.projection, buildChatReceiptProjection(epoch.manifest, states));
|
|
21765
|
+
displayedProjection = mergeChatReceiptProjections(displayedProjection, sourceProjection);
|
|
21689
21766
|
onUpdate?.(projectChatReceiptCheckpoint(epoch.manifest, {
|
|
21690
|
-
projection:
|
|
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
|
-
|
|
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
|
-
|
|
23215
|
-
|
|
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
|
-
|
|
23443
|
-
|
|
23444
|
-
|
|
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
|
|
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
|
|
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
|
|
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);
|