@gryt/core 0.18.2 → 0.20.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/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/mls/content.d.ts +22 -0
- package/dist/mls/content.js +57 -2
- package/dist/mls/deliveryService.fake.d.ts +2 -0
- package/dist/mls/deliveryService.fake.js +26 -1
- package/dist/mls/dmDriver.js +72 -2
- package/dist/mls/interfaces.d.ts +37 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -12,9 +12,9 @@ export { describeFolderRules, folderFollowNote, folderPhrase, } from "./permissi
|
|
|
12
12
|
export { agreementAt, createTermsGate, GUIDELINES_URL, TERMS_STORAGE_KEY, TERMS_URL, TERMS_VERSION, } from "./terms/termsAgreement.js";
|
|
13
13
|
export { createMlsDmDriver, MlsDriverError, type MlsDriverErrorCode } from "./mls/dmDriver.js";
|
|
14
14
|
export { mlsPinsFromPeerPins } from "./mls/pins.js";
|
|
15
|
-
export { decodeMlsDmContent, encodeMlsDmContent, MLS_DM_CONTENT_VERSION, type MlsDmContent, readMlsDmContent, } from "./mls/content.js";
|
|
15
|
+
export { decodeMlsDmContent, encodeMlsDmContent, historyBatches, MAX_HISTORY_BATCH_BYTES, MLS_DM_CONTENT_VERSION, type MlsDmContent, type MlsHistoryEntry, readMlsDmContent, } from "./mls/content.js";
|
|
16
16
|
export { applyMlsReaction, mlsReactionAction, type MlsReaction } from "./mls/reactions.js";
|
|
17
|
-
export type { DmSealingMode, MlsAddOwnDeviceOptions, MlsClaimedKeyPackage, MlsDecryptedMessage, MlsDeviceRecord, MlsDeviceRef, MlsDmDriver, MlsDmDriverOptions, MlsDmEvents, MlsGroupPosition, MlsGroupRecord, MlsGroupView, MlsKeyPackageRecord, MlsListedDevice, MlsLogEntry, MlsOwnDevice, MlsOwnDeviceAdd, MlsPins, MlsRefusal, MlsReply, MlsServerCapability, MlsStateStore, MlsTransport, MlsWelcomeDelivery, } from "./mls/interfaces.js";
|
|
17
|
+
export type { DmSealingMode, MlsAddOwnDeviceOptions, MlsClaimedKeyPackage, MlsDecryptedMessage, MlsDeviceRecord, MlsDeviceRef, MlsDmDriver, MlsDmDriverOptions, MlsDmEvents, MlsGroupPosition, MlsGroupRecord, MlsGroupView, MlsKeyPackageRecord, MlsListedDevice, MlsLogEntry, MlsDmDiagnostics, MlsOwnDevice, MlsOwnDeviceAdd, MlsPins, MlsRefusal, MlsReply, MlsServerCapability, MlsStateStore, MlsTransport, MlsWelcomeDelivery, } from "./mls/interfaces.js";
|
|
18
18
|
export { createApproverPairing, type ApproverOptions, type ApproverPairing, type ApproverState, } from "./pairing/approver.js";
|
|
19
19
|
export { createNewDevicePairing, PAIRING_APPROVAL_MS, type NewDeviceOptions, type NewDevicePairing, type NewDeviceState, } from "./pairing/newDevice.js";
|
|
20
20
|
export { createPairingRelay, PairingRelayError, type PairingRelay } from "./pairing/relayClient.js";
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { describeFolderRules, folderFollowNote, folderPhrase, } from "./permissi
|
|
|
14
14
|
export { agreementAt, createTermsGate, GUIDELINES_URL, TERMS_STORAGE_KEY, TERMS_URL, TERMS_VERSION, } from "./terms/termsAgreement.js";
|
|
15
15
|
export { createMlsDmDriver, MlsDriverError } from "./mls/dmDriver.js";
|
|
16
16
|
export { mlsPinsFromPeerPins } from "./mls/pins.js";
|
|
17
|
-
export { decodeMlsDmContent, encodeMlsDmContent, MLS_DM_CONTENT_VERSION, readMlsDmContent, } from "./mls/content.js";
|
|
17
|
+
export { decodeMlsDmContent, encodeMlsDmContent, historyBatches, MAX_HISTORY_BATCH_BYTES, MLS_DM_CONTENT_VERSION, readMlsDmContent, } from "./mls/content.js";
|
|
18
18
|
export { applyMlsReaction, mlsReactionAction } from "./mls/reactions.js";
|
|
19
19
|
export { createApproverPairing, } from "./pairing/approver.js";
|
|
20
20
|
export { createNewDevicePairing, PAIRING_APPROVAL_MS, } from "./pairing/newDevice.js";
|
package/dist/mls/content.d.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { type SealedAttachmentKey } from "@gryt/crypto";
|
|
2
|
+
/** One message from the sender's own record of the DM, for the other person's new device (GRYT-1875). */
|
|
3
|
+
export interface MlsHistoryEntry {
|
|
4
|
+
id: string;
|
|
5
|
+
/** Server user id of whoever wrote it, as the sender's record has it. */
|
|
6
|
+
senderId: string;
|
|
7
|
+
/** Milliseconds since the epoch. */
|
|
8
|
+
sentAt: number;
|
|
9
|
+
text: string;
|
|
10
|
+
replyTo?: string;
|
|
11
|
+
attachments?: Record<string, SealedAttachmentKey>;
|
|
12
|
+
}
|
|
2
13
|
export type MlsDmContent = {
|
|
14
|
+
type: "history";
|
|
15
|
+
/** For the batch itself, so a repeat is the same message. */
|
|
16
|
+
id: string;
|
|
17
|
+
entries: MlsHistoryEntry[];
|
|
18
|
+
} | {
|
|
3
19
|
type: "message";
|
|
4
20
|
/** Made by the sender, a UUID. Edits and deletes name it. */
|
|
5
21
|
id: string;
|
|
@@ -24,8 +40,14 @@ export type MlsDmContent = {
|
|
|
24
40
|
action: "add" | "remove";
|
|
25
41
|
};
|
|
26
42
|
export declare const MLS_DM_CONTENT_VERSION = 1;
|
|
43
|
+
/** Under what one `mls:send` takes once sealed and padded. */
|
|
44
|
+
export declare const MAX_HISTORY_BATCH_BYTES: number;
|
|
27
45
|
/** The same bytes the phone writes: `v` first, then the content's fields in this order. */
|
|
28
46
|
export declare function encodeMlsDmContent(content: MlsDmContent): Uint8Array;
|
|
47
|
+
/** History as batches small enough to send, oldest first. One entry too big to send on its own is left out. */
|
|
48
|
+
export declare function historyBatches(entries: MlsHistoryEntry[], newId: () => string): Extract<MlsDmContent, {
|
|
49
|
+
type: "history";
|
|
50
|
+
}>[];
|
|
29
51
|
/** Null for anything this version can't read: a newer version, a new type, or junk. */
|
|
30
52
|
export declare function decodeMlsDmContent(bytes: Uint8Array): MlsDmContent | null;
|
|
31
53
|
/**
|
package/dist/mls/content.js
CHANGED
|
@@ -16,6 +16,9 @@ const isId = (v) => typeof v === "string" && v.length > 0 && v.length <= MAX_ID;
|
|
|
16
16
|
const isText = (v) => typeof v === "string" && v.length <= MAX_TEXT;
|
|
17
17
|
const isCount = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
18
18
|
const isLabel = (v) => typeof v === "string" && v.length <= MAX_LABEL;
|
|
19
|
+
const MAX_HISTORY_ENTRIES = 200;
|
|
20
|
+
/** Under what one `mls:send` takes once sealed and padded. */
|
|
21
|
+
export const MAX_HISTORY_BATCH_BYTES = 48 * 1024;
|
|
19
22
|
/** One emoji's worth of emoji characters, so a peer can't put a sentence on a message as a reaction. */
|
|
20
23
|
function isReactionEmoji(v) {
|
|
21
24
|
if (typeof v !== "string")
|
|
@@ -79,7 +82,10 @@ function readAttachments(v) {
|
|
|
79
82
|
/** The same bytes the phone writes: `v` first, then the content's fields in this order. */
|
|
80
83
|
export function encodeMlsDmContent(content) {
|
|
81
84
|
let body;
|
|
82
|
-
if (content.type === "
|
|
85
|
+
if (content.type === "history") {
|
|
86
|
+
body = { type: content.type, id: content.id, entries: content.entries.map(historyBody) };
|
|
87
|
+
}
|
|
88
|
+
else if (content.type === "message") {
|
|
83
89
|
body = { type: content.type, id: content.id, text: content.text };
|
|
84
90
|
if (content.replyTo !== undefined)
|
|
85
91
|
body.replyTo = content.replyTo;
|
|
@@ -100,6 +106,49 @@ export function encodeMlsDmContent(content) {
|
|
|
100
106
|
throw new Error("That content wouldn't read back, so it isn't sent.");
|
|
101
107
|
return bytes;
|
|
102
108
|
}
|
|
109
|
+
function historyBody(e) {
|
|
110
|
+
const out = { id: e.id, senderId: e.senderId, sentAt: e.sentAt, text: e.text };
|
|
111
|
+
if (e.replyTo !== undefined)
|
|
112
|
+
out.replyTo = e.replyTo;
|
|
113
|
+
if (e.attachments && Object.keys(e.attachments).length > 0)
|
|
114
|
+
out.attachments = e.attachments;
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
/** History as batches small enough to send, oldest first. One entry too big to send on its own is left out. */
|
|
118
|
+
export function historyBatches(entries, newId) {
|
|
119
|
+
const size = (batch) => new TextEncoder().encode(JSON.stringify({ v: MLS_DM_CONTENT_VERSION, type: "history", id: "x".repeat(MAX_ID), entries: batch.map(historyBody) })).length;
|
|
120
|
+
const out = [];
|
|
121
|
+
let batch = [];
|
|
122
|
+
for (const e of [...entries].sort((a, b) => a.sentAt - b.sentAt)) {
|
|
123
|
+
if (!readHistoryEntry(historyBody(e)) || size([e]) > MAX_HISTORY_BATCH_BYTES)
|
|
124
|
+
continue;
|
|
125
|
+
if (batch.length === MAX_HISTORY_ENTRIES || size([...batch, e]) > MAX_HISTORY_BATCH_BYTES) {
|
|
126
|
+
out.push({ type: "history", id: newId(), entries: batch });
|
|
127
|
+
batch = [];
|
|
128
|
+
}
|
|
129
|
+
batch.push(e);
|
|
130
|
+
}
|
|
131
|
+
if (batch.length)
|
|
132
|
+
out.push({ type: "history", id: newId(), entries: batch });
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
function readHistoryEntry(v) {
|
|
136
|
+
if (!isObject(v) || !isId(v.id) || !isId(v.senderId) || !isText(v.text) || !isCount(v.sentAt))
|
|
137
|
+
return null;
|
|
138
|
+
const out = { id: v.id, senderId: v.senderId, sentAt: v.sentAt, text: v.text };
|
|
139
|
+
if (v.replyTo !== undefined) {
|
|
140
|
+
if (!isId(v.replyTo))
|
|
141
|
+
return null;
|
|
142
|
+
out.replyTo = v.replyTo;
|
|
143
|
+
}
|
|
144
|
+
if (v.attachments !== undefined) {
|
|
145
|
+
const attachments = readAttachments(v.attachments);
|
|
146
|
+
if (!attachments)
|
|
147
|
+
return null;
|
|
148
|
+
out.attachments = attachments;
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
103
152
|
/** Null for anything this version can't read: a newer version, a new type, or junk. */
|
|
104
153
|
export function decodeMlsDmContent(bytes) {
|
|
105
154
|
return fromRaw(parse(bytes));
|
|
@@ -112,7 +161,7 @@ export function readMlsDmContent(bytes) {
|
|
|
112
161
|
const raw = parse(bytes);
|
|
113
162
|
return fromRaw(raw) ?? (isNewer(raw) ? "newer" : null);
|
|
114
163
|
}
|
|
115
|
-
const KNOWN_TYPES = new Set(["message", "edit", "delete", "reaction"]);
|
|
164
|
+
const KNOWN_TYPES = new Set(["message", "edit", "delete", "reaction", "history"]);
|
|
116
165
|
const TYPE_NAME = /^[a-z][a-z0-9_]{0,31}$/;
|
|
117
166
|
/** A later version, or a type this one doesn't know that names its message the way ours do. */
|
|
118
167
|
function isNewer(raw) {
|
|
@@ -134,6 +183,12 @@ function fromRaw(raw) {
|
|
|
134
183
|
if (!isObject(raw) || raw.v !== MLS_DM_CONTENT_VERSION || !isId(raw.id))
|
|
135
184
|
return null;
|
|
136
185
|
const { type, id, text } = raw;
|
|
186
|
+
if (type === "history") {
|
|
187
|
+
if (!Array.isArray(raw.entries) || raw.entries.length === 0 || raw.entries.length > MAX_HISTORY_ENTRIES)
|
|
188
|
+
return null;
|
|
189
|
+
const entries = raw.entries.map(readHistoryEntry);
|
|
190
|
+
return entries.every((e) => e !== null) ? { type, id, entries } : null;
|
|
191
|
+
}
|
|
137
192
|
if (type === "message" && isText(text)) {
|
|
138
193
|
const out = { type, id, text };
|
|
139
194
|
if (raw.replyTo !== undefined) {
|
|
@@ -75,6 +75,8 @@ export declare class FakeDeliveryService {
|
|
|
75
75
|
private devicesOf;
|
|
76
76
|
private isDevice;
|
|
77
77
|
private groupView;
|
|
78
|
+
/** What the server's retention sweep does to a device not seen for a while: gone, but not removed. */
|
|
79
|
+
expire(serverUserId: string, deviceId: string): void;
|
|
78
80
|
private transportFor;
|
|
79
81
|
private checkParts;
|
|
80
82
|
private counts;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* An in-memory stand-in for the delivery service in Gryt-chat/server#241, for tests only.
|
|
2
2
|
Same answers and pushes; no auth, blocks, rate limits or member checks on adds. */
|
|
3
|
-
import { base64UrlDecode, inspectMlsMessage, mlsWelcomeRefs, readMlsKeyPackage } from "@gryt/crypto";
|
|
3
|
+
import { base64UrlDecode, inspectMlsMessage, mlsGroupInfoDeviceIds, mlsWelcomeRefs, readMlsKeyPackage, } from "@gryt/crypto";
|
|
4
4
|
const MAX_DEVICES = 100;
|
|
5
5
|
const MAX_KEY_PACKAGES = 20;
|
|
6
6
|
const MAX_LOG_PAGE = 200;
|
|
@@ -85,6 +85,13 @@ export class FakeDeliveryService {
|
|
|
85
85
|
groupView(g) {
|
|
86
86
|
return { conversationId: g.conversationId, groupId: g.groupId, epoch: g.epoch, headSeq: g.headSeq };
|
|
87
87
|
}
|
|
88
|
+
/** What the server's retention sweep does to a device not seen for a while: gone, but not removed. */
|
|
89
|
+
expire(serverUserId, deviceId) {
|
|
90
|
+
this.devices.set(serverUserId, (this.devices.get(serverUserId) ?? []).filter((d) => d !== deviceId));
|
|
91
|
+
this.keyPackages = this.keyPackages.filter((k) => !(k.serverUserId === serverUserId && k.deviceId === deviceId));
|
|
92
|
+
this.welcomes = this.welcomes.filter((w) => !(w.serverUserId === serverUserId && w.deviceId === deviceId));
|
|
93
|
+
this.push(this.peopleSharing(serverUserId), (d) => d.handleDevicesChanged({ serverUserId }));
|
|
94
|
+
}
|
|
88
95
|
transportFor(socket) {
|
|
89
96
|
const me = socket.serverUserId;
|
|
90
97
|
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
@@ -276,6 +283,24 @@ export class FakeDeliveryService {
|
|
|
276
283
|
g.info = { epoch: g.epoch, data: groupInfo.slice() };
|
|
277
284
|
return { ok: true };
|
|
278
285
|
},
|
|
286
|
+
replaceGroup: async ({ conversationId, deviceId, oldGroupId, groupId }) => {
|
|
287
|
+
await tick();
|
|
288
|
+
if (!member(conversationId))
|
|
289
|
+
return fail("not_found", "No such conversation.");
|
|
290
|
+
if (!this.isDevice(me, deviceId))
|
|
291
|
+
return fail("unknown_device", "Publish KeyPackages from this device first.");
|
|
292
|
+
const g = this.groups.get(conversationId);
|
|
293
|
+
if (!g || g.groupId !== oldGroupId)
|
|
294
|
+
return fail("stale_group", "That isn't this conversation's group anymore.");
|
|
295
|
+
const listed = new Set(this.devicesOf(this.conversations.get(conversationId)).map((d) => d.deviceId));
|
|
296
|
+
if (!g.info || mlsGroupInfoDeviceIds(g.info.data, this.scope).some((id) => listed.has(id))) {
|
|
297
|
+
return fail("not_abandoned", "Somebody in this group can still add devices.");
|
|
298
|
+
}
|
|
299
|
+
const fresh = { conversationId, groupId, epoch: 0, headSeq: 0, log: [] };
|
|
300
|
+
this.groups.set(conversationId, fresh);
|
|
301
|
+
this.welcomes = this.welcomes.filter((w) => w.groupId !== oldGroupId);
|
|
302
|
+
return { ok: true, group: this.groupView(fresh) };
|
|
303
|
+
},
|
|
279
304
|
getGroupInfo: async ({ conversationId, deviceId }) => {
|
|
280
305
|
await tick();
|
|
281
306
|
if (!member(conversationId))
|
package/dist/mls/dmDriver.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { addMlsMembers, asIdentityScope, base64UrlDecode, createMlsGroup, decodeMlsGroupState, encodeMlsGroupState, encryptMlsMessage, forgetMlsSecrets, generateMlsKeyPackage, joinMlsGroup, joinMlsGroupExternally, mlsGroupInfo, mlsGroupInfoForJoining, mlsGroupMembers, mlsWelcomeRefs, processMlsMessage, readDeviceCertificate, readMlsKeyPackage, removeMlsMembers, } from "@gryt/crypto";
|
|
1
|
+
import { addMlsMembers, asIdentityScope, base64UrlDecode, createMlsGroup, decodeMlsGroupState, encodeMlsGroupState, encryptMlsMessage, forgetMlsSecrets, generateMlsKeyPackage, joinMlsGroup, joinMlsGroupExternally, mlsGroupInfo, mlsGroupInfoForJoining, mlsGroupInfoDeviceIds, mlsGroupMembers, mlsWelcomeRefs, processMlsMessage, readDeviceCertificate, readMlsKeyPackage, removeMlsMembers, } from "@gryt/crypto";
|
|
2
2
|
/** Devices per person per server. Must match the server's MLS_MAX_DEVICES, or extra devices are left out quietly. */
|
|
3
3
|
const MLS_MAX_DEVICES_PER_PERSON = 100;
|
|
4
4
|
const COMMIT_ATTEMPTS = 5;
|
|
@@ -92,6 +92,7 @@ export function createMlsDmDriver(options) {
|
|
|
92
92
|
const rawRequestAdd = raw.requestAdd?.bind(raw);
|
|
93
93
|
const rawPutGroupInfo = raw.putGroupInfo?.bind(raw);
|
|
94
94
|
const rawGetGroupInfo = raw.getGroupInfo?.bind(raw);
|
|
95
|
+
const rawReplaceGroup = raw.replaceGroup?.bind(raw);
|
|
95
96
|
const transport = {
|
|
96
97
|
publishKeyPackages: stopOnRemoval((req) => raw.publishKeyPackages(req)),
|
|
97
98
|
claimKeyPackages: stopOnRemoval((req) => raw.claimKeyPackages(req)),
|
|
@@ -106,6 +107,7 @@ export function createMlsDmDriver(options) {
|
|
|
106
107
|
...(rawRequestAdd ? { requestAdd: stopOnRemoval(rawRequestAdd) } : {}),
|
|
107
108
|
...(rawPutGroupInfo ? { putGroupInfo: stopOnRemoval(rawPutGroupInfo) } : {}),
|
|
108
109
|
...(rawGetGroupInfo ? { getGroupInfo: stopOnRemoval(rawGetGroupInfo) } : {}),
|
|
110
|
+
...(rawReplaceGroup ? { replaceGroup: stopOnRemoval(rawReplaceGroup) } : {}),
|
|
109
111
|
};
|
|
110
112
|
const scope = asIdentityScope(options.scope);
|
|
111
113
|
const trusts = new Map();
|
|
@@ -527,7 +529,7 @@ export function createMlsDmDriver(options) {
|
|
|
527
529
|
const list = welcomeWaiters.get(conversationId) ?? [];
|
|
528
530
|
welcomeWaiters.set(conversationId, list.filter((w) => w !== done));
|
|
529
531
|
reject(new MlsDriverError("waiting_for_welcome", "The group exists and this device hasn't been added yet."));
|
|
530
|
-
}, WELCOME_WAIT_MS);
|
|
532
|
+
}, options.welcomeWaitMs ?? WELCOME_WAIT_MS);
|
|
531
533
|
welcomeWaiters.set(conversationId, [...(welcomeWaiters.get(conversationId) ?? []), done]);
|
|
532
534
|
});
|
|
533
535
|
}
|
|
@@ -578,6 +580,71 @@ export function createMlsDmDriver(options) {
|
|
|
578
580
|
return true;
|
|
579
581
|
});
|
|
580
582
|
}
|
|
583
|
+
/* Every device in the group is gone, so nobody can add anyone and nobody can join by itself:
|
|
584
|
+
this device starts a new group in its place. Others who held the old one follow it (core#48). */
|
|
585
|
+
async function rebuild(conversationId) {
|
|
586
|
+
const replace = transport.replaceGroup;
|
|
587
|
+
if (!replace || !transport.getGroupInfo)
|
|
588
|
+
return false;
|
|
589
|
+
const d = await ownDevice();
|
|
590
|
+
const got = await transport.getGroupInfo({ conversationId, deviceId: d.deviceId }).catch(() => null);
|
|
591
|
+
const listed = got?.ok ? await transport.listDevices({ conversationId }) : null;
|
|
592
|
+
const log = listed?.ok ? await transport.fetchLog({ conversationId, after: 0, limit: 1 }) : null;
|
|
593
|
+
if (!got?.ok || !listed?.ok || !log?.ok || !log.group)
|
|
594
|
+
return false;
|
|
595
|
+
const inGroup = mlsGroupInfoDeviceIds(got.groupInfo, scope);
|
|
596
|
+
if (inGroup.some((id) => listed.devices.some((x) => x.deviceId === id)))
|
|
597
|
+
return false;
|
|
598
|
+
const oldGroupId = log.group.groupId;
|
|
599
|
+
return run(conversationId, async () => {
|
|
600
|
+
if (await store.loadGroup(conversationId))
|
|
601
|
+
return true;
|
|
602
|
+
const state = await createMlsGroup(d, trustFor(conversationId));
|
|
603
|
+
const open = {
|
|
604
|
+
rec: { conversationId, groupId: groupIdOf(state), state: encodeMlsGroupState(state), cursor: 0, joinedEpoch: 0 },
|
|
605
|
+
state,
|
|
606
|
+
};
|
|
607
|
+
await store.saveGroup(open.rec);
|
|
608
|
+
const r = await replace({ conversationId, deviceId: d.deviceId, oldGroupId, groupId: open.rec.groupId });
|
|
609
|
+
if (!r.ok) {
|
|
610
|
+
await store.deleteGroup(conversationId);
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
void publishGroupInfo(open);
|
|
614
|
+
return true;
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
async function diagnose(conversationId) {
|
|
618
|
+
const d = device ?? (await store.loadDevice());
|
|
619
|
+
const rec = await store.loadGroup(conversationId);
|
|
620
|
+
let inGroup = null;
|
|
621
|
+
let epoch = -1;
|
|
622
|
+
if (rec) {
|
|
623
|
+
try {
|
|
624
|
+
const state = decodeMlsGroupState(rec.state, trustFor(conversationId));
|
|
625
|
+
epoch = epochOf(state);
|
|
626
|
+
inGroup = mlsGroupMembers(state, scope).map((m) => ({
|
|
627
|
+
deviceId: m.certificate.deviceId,
|
|
628
|
+
name: m.certificate.deviceName,
|
|
629
|
+
thisDevice: m.certificate.deviceId === d?.deviceId,
|
|
630
|
+
}));
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
inGroup = null;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const listed = await transport.listDevices({ conversationId }).catch(() => null);
|
|
637
|
+
return {
|
|
638
|
+
deviceId: d?.deviceId ?? null,
|
|
639
|
+
registered,
|
|
640
|
+
group: rec ? { groupId: rec.groupId, epoch, cursor: rec.cursor, joinedEpoch: rec.joinedEpoch } : null,
|
|
641
|
+
inGroup,
|
|
642
|
+
listed: listed?.ok ? listed.devices : null,
|
|
643
|
+
left: left.has(conversationId),
|
|
644
|
+
unjoinable: unjoinable.has(conversationId),
|
|
645
|
+
askedAt: asked.get(conversationId) ?? null,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
581
648
|
/* Only a member can add a device, so one that's out asks the members' apps to. An old
|
|
582
649
|
server doesn't answer, which is why nothing waits on it. */
|
|
583
650
|
async function askToBeAdded(conversationId, force = false) {
|
|
@@ -613,6 +680,8 @@ export function createMlsDmDriver(options) {
|
|
|
613
680
|
}
|
|
614
681
|
if (await joinByItself(conversationId))
|
|
615
682
|
return;
|
|
683
|
+
if (await rebuild(conversationId))
|
|
684
|
+
return;
|
|
616
685
|
void askToBeAdded(conversationId).catch(() => undefined);
|
|
617
686
|
await waitForWelcome(conversationId);
|
|
618
687
|
}
|
|
@@ -1052,6 +1121,7 @@ export function createMlsDmDriver(options) {
|
|
|
1052
1121
|
handleAddRequest,
|
|
1053
1122
|
requestAdd: (conversationId) => askToBeAdded(conversationId, true),
|
|
1054
1123
|
ownDevices,
|
|
1124
|
+
diagnose,
|
|
1055
1125
|
async removeOwnDevice(deviceId) {
|
|
1056
1126
|
const r = await transport.removeDevice({ deviceId });
|
|
1057
1127
|
if (!r.ok)
|
package/dist/mls/interfaces.d.ts
CHANGED
|
@@ -159,6 +159,15 @@ export interface MlsTransport {
|
|
|
159
159
|
epoch: number;
|
|
160
160
|
groupInfo: Uint8Array;
|
|
161
161
|
}>>;
|
|
162
|
+
/** A fresh group in place of one nobody is left in to add anyone (GRYT-1862). The server checks that. */
|
|
163
|
+
replaceGroup?(req: {
|
|
164
|
+
conversationId: string;
|
|
165
|
+
deviceId: string;
|
|
166
|
+
oldGroupId: string;
|
|
167
|
+
groupId: string;
|
|
168
|
+
}): Promise<MlsReply<{
|
|
169
|
+
group: MlsGroupView;
|
|
170
|
+
}>>;
|
|
162
171
|
}
|
|
163
172
|
/** `server:info.mls`. Absent means the server can't do MLS. */
|
|
164
173
|
export interface MlsServerCapability {
|
|
@@ -334,6 +343,32 @@ export interface MlsDmDriverOptions {
|
|
|
334
343
|
newDevice(): MlsDeviceRecord | Promise<MlsDeviceRecord>;
|
|
335
344
|
/** How long a send waits for the server before it fails with `offline`. Five minutes. */
|
|
336
345
|
sendGiveUpMs?: number;
|
|
346
|
+
/** How long a send waits for this device to be added to a group that exists. Ten seconds. */
|
|
347
|
+
welcomeWaitMs?: number;
|
|
348
|
+
}
|
|
349
|
+
/** One DM as this device sees it, for support (GRYT-1864). Ids and counts, never message content. */
|
|
350
|
+
export interface MlsDmDiagnostics {
|
|
351
|
+
/** Null when this device has no MLS record here yet. */
|
|
352
|
+
deviceId: string | null;
|
|
353
|
+
registered: boolean;
|
|
354
|
+
group: {
|
|
355
|
+
groupId: string;
|
|
356
|
+
epoch: number;
|
|
357
|
+
cursor: number;
|
|
358
|
+
joinedEpoch: number;
|
|
359
|
+
} | null;
|
|
360
|
+
/** The devices in this device's copy of the group, named by their certificates. */
|
|
361
|
+
inGroup: {
|
|
362
|
+
deviceId: string;
|
|
363
|
+
name: string | null;
|
|
364
|
+
thisDevice: boolean;
|
|
365
|
+
}[] | null;
|
|
366
|
+
/** What the server lists for the people in this DM, or null when it didn't answer. */
|
|
367
|
+
listed: MlsListedDevice[] | null;
|
|
368
|
+
left: boolean;
|
|
369
|
+
unjoinable: boolean;
|
|
370
|
+
/** When this device last asked to be added, in ms. */
|
|
371
|
+
askedAt: number | null;
|
|
337
372
|
}
|
|
338
373
|
/** One per server. Calls for one conversation run one at a time, in order. */
|
|
339
374
|
export interface MlsDmDriver {
|
|
@@ -364,6 +399,8 @@ export interface MlsDmDriver {
|
|
|
364
399
|
requestAdd(conversationId: string): Promise<void>;
|
|
365
400
|
/** Oldest first, as the server lists them. */
|
|
366
401
|
ownDevices(): Promise<MlsOwnDevice[]>;
|
|
402
|
+
/** See `MlsDmDiagnostics`. Creates nothing, so looking can't change what it shows. */
|
|
403
|
+
diagnose(conversationId: string): Promise<MlsDmDiagnostics>;
|
|
367
404
|
/** Your own devices only. Every group you share drops it on the next pass. */
|
|
368
405
|
removeOwnDevice(deviceId: string): Promise<void>;
|
|
369
406
|
/** Pairing: each group's cursor now, which is what the history snapshot covers. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gryt/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Gryt's shared application logic: one implementation of the things the desktop and mobile apps both do.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@gryt/crypto": "0.
|
|
46
|
+
"@gryt/crypto": "0.13.0",
|
|
47
47
|
"@noble/hashes": "^2.3.0"
|
|
48
48
|
}
|
|
49
49
|
}
|